2026-10-23 14:47:27 +08:00
|
|
|
import json
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
|
|
from pigway_frame_flow.models import JobInput
|
|
|
|
|
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def source(service, settings, legacy_photo_engine):
|
|
|
|
|
root = settings.data_dir.parent / "photos"
|
|
|
|
|
root.mkdir()
|
|
|
|
|
settings.photo_root = root
|
|
|
|
|
image = Image.new("RGB", (900, 300), "red")
|
|
|
|
|
image.paste("blue", (450, 0, 900, 300))
|
|
|
|
|
image.save(root / "photo.jpg")
|
|
|
|
|
service.photo_sources.save(
|
|
|
|
|
"family", PhotoSourceInput(name="Family", kind="local", location=".")
|
|
|
|
|
)
|
|
|
|
|
return {"source_id": "family", "item_id": "photo.jpg"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_plan_crud_and_parameter_validation(client):
|
|
|
|
|
assert client.get("/ui/thumbnails.js").status_code == 200
|
|
|
|
|
plan = client.post("/api/v1/thumbnail-plans", json={"name": "Watch"}).json()
|
|
|
|
|
path = "/api/v1/thumbnail-plans/" + plan["id"]
|
|
|
|
|
assert client.get(path).json()["width"] == 410
|
|
|
|
|
assert len(client.get("/api/v1/thumbnail-plans").json()) == 1
|
|
|
|
|
assert (
|
|
|
|
|
client.put(
|
|
|
|
|
path, json={"name": "Square", "width": 240, "height": 240}
|
|
|
|
|
).status_code
|
|
|
|
|
== 200
|
|
|
|
|
)
|
|
|
|
|
for changes in [
|
|
|
|
|
{"width": 0},
|
|
|
|
|
{"height": 10000},
|
|
|
|
|
{"center_x": 2},
|
|
|
|
|
{"quality": 100},
|
|
|
|
|
{"fit": "stretch"},
|
|
|
|
|
{"extra": True},
|
|
|
|
|
]:
|
|
|
|
|
assert client.put(path, json={"name": "Invalid", **changes}).status_code == 422
|
|
|
|
|
assert client.delete(path).status_code == 204
|
|
|
|
|
assert client.get(path).status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_preview_matches_generated_pixels_and_crop(client, source):
|
|
|
|
|
request = {
|
|
|
|
|
**source,
|
|
|
|
|
"plan": {"name": "Right", "width": 120, "height": 180, "center_x": 1},
|
|
|
|
|
}
|
|
|
|
|
preview = client.post("/api/v1/thumbnails/preview", json=request)
|
|
|
|
|
assert preview.status_code == 200
|
|
|
|
|
assert preview.headers["cache-control"] == "no-store"
|
|
|
|
|
asset = client.post("/api/v1/thumbnails/generate", json=request).json()
|
|
|
|
|
assert client.get(asset["url"]).content == preview.content
|
|
|
|
|
with Image.open(BytesIO(preview.content)) as image:
|
|
|
|
|
assert image.size == (120, 180)
|
|
|
|
|
assert image.getpixel((60, 90))[2] > 240
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_task_requires_one_existing_plan_and_blocks_delete(client, app, source):
|
|
|
|
|
plan = client.post("/api/v1/thumbnail-plans", json={"name": "Watch"}).json()
|
|
|
|
|
job = {
|
|
|
|
|
"name": "Photos",
|
|
|
|
|
"schedule": {"type": "daily", "time": "07:00"},
|
|
|
|
|
"source": {"type": "photo_source", "source_id": source["source_id"]},
|
|
|
|
|
"generate_thumbnail": True,
|
|
|
|
|
"thumbnail_plan_id": plan["id"],
|
|
|
|
|
}
|
|
|
|
|
saved = client.post("/api/v1/jobs", json=job)
|
|
|
|
|
assert saved.status_code == 201
|
|
|
|
|
assert saved.json()["thumbnail_plan_id"] == plan["id"]
|
|
|
|
|
service = app.state.service
|
|
|
|
|
now = datetime.now(UTC)
|
|
|
|
|
service.db.execute(
|
|
|
|
|
"UPDATE jobs SET next_run=? WHERE id=?", (now.isoformat(), saved.json()["id"])
|
|
|
|
|
)
|
|
|
|
|
app.state.scheduler.tick(now)
|
|
|
|
|
pending = service.db.one("SELECT id,payload FROM operations WHERE action='display'")
|
|
|
|
|
assert json.loads(pending["payload"])["options"]["thumbnail_plan_id"] == plan["id"]
|
|
|
|
|
assert service.process_one()
|
|
|
|
|
assert (
|
|
|
|
|
service.operation(pending["id"])["result"]["wrist_thumbnail"]["plan_id"]
|
|
|
|
|
== plan["id"]
|
|
|
|
|
)
|
|
|
|
|
assert client.delete("/api/v1/thumbnail-plans/" + plan["id"]).status_code == 409
|
|
|
|
|
assert (
|
|
|
|
|
client.post("/api/v1/jobs", json={**job, "thumbnail_plan_id": None}).status_code
|
|
|
|
|
== 422
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
client.post(
|
|
|
|
|
"/api/v1/jobs", json={**job, "thumbnail_plan_id": "0" * 32}
|
|
|
|
|
).status_code
|
|
|
|
|
== 404
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
client.post(
|
|
|
|
|
"/api/v1/jobs",
|
|
|
|
|
json={**job, "source": {"type": "template", "template": "quote"}},
|
|
|
|
|
).status_code
|
|
|
|
|
== 422
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
JobInput.model_validate(
|
|
|
|
|
{
|
|
|
|
|
k: v
|
|
|
|
|
for k, v in job.items()
|
|
|
|
|
if k not in ("generate_thumbnail", "thumbnail_plan_id")
|
|
|
|
|
}
|
|
|
|
|
).generate_thumbnail
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("enabled", [True, False])
|
|
|
|
|
def test_upload_uses_selected_plan_or_skips(service, client, source, enabled):
|
|
|
|
|
plan = client.post(
|
|
|
|
|
"/api/v1/thumbnail-plans", json={"name": "Square", "width": 128, "height": 128}
|
|
|
|
|
).json()
|
|
|
|
|
operation = service.submit(
|
|
|
|
|
"display",
|
|
|
|
|
{
|
|
|
|
|
"content": {"type": "photo_source", "source_id": "family"},
|
|
|
|
|
"options": {
|
|
|
|
|
"priority": 0,
|
|
|
|
|
"generate_thumbnail": enabled,
|
|
|
|
|
"thumbnail_plan_id": plan["id"] if enabled else None,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"living-room",
|
|
|
|
|
"test",
|
|
|
|
|
"request",
|
|
|
|
|
)
|
|
|
|
|
service.process_one()
|
|
|
|
|
result = service.operation(operation)["result"]
|
|
|
|
|
if enabled:
|
|
|
|
|
assert result["wrist_thumbnail"]["width"] == 128
|
|
|
|
|
assert result["wrist_thumbnail"]["plan_id"] == plan["id"]
|
|
|
|
|
else:
|
|
|
|
|
assert "wrist_thumbnail" not in result
|
2026-10-23 15:00:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_watch_gallery_separates_thumbnails_and_deletes_latest(client, service, source):
|
|
|
|
|
original = service.assets.put(b"fixture", "normal.jpg", "images")
|
|
|
|
|
generated = client.post(
|
|
|
|
|
"/api/v1/thumbnails/generate", json={**source, "plan": {"name": "Watch"}}
|
|
|
|
|
).json()
|
|
|
|
|
operation = service.submit(
|
|
|
|
|
"display",
|
|
|
|
|
{
|
|
|
|
|
"content": {"type": "photo_source", "source_id": "family"},
|
|
|
|
|
"options": {"priority": 0},
|
|
|
|
|
},
|
|
|
|
|
"living-room",
|
|
|
|
|
"test",
|
|
|
|
|
"request",
|
|
|
|
|
)
|
|
|
|
|
service.process_one()
|
|
|
|
|
auto = service.operation(operation)["result"]["wrist_thumbnail"]
|
|
|
|
|
page = client.get("/api/v1/thumbnails?limit=1").json()
|
|
|
|
|
assert page["total"] == 2 and len(page["items"]) == 1
|
|
|
|
|
items = client.get("/api/v1/thumbnails").json()["items"]
|
|
|
|
|
assert {x["asset_id"] for x in items} == {generated["asset_id"], auto["asset_id"]}
|
|
|
|
|
assert all(x["thumbnail"]["target_id"] == "watch" for x in items)
|
|
|
|
|
assert {x["asset_id"] for x in client.get("/api/v1/assets").json()} == {
|
|
|
|
|
original["asset_id"]
|
|
|
|
|
}
|
|
|
|
|
assert (
|
|
|
|
|
client.get("/api/v1/thumbnail-devices").json()[0]["transport_connected"]
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
assert client.delete("/api/v1/assets/" + auto["asset_id"]).status_code == 204
|
|
|
|
|
assert client.get("/api/v1/thumbnails").json()["total"] == 1
|
|
|
|
|
assert (
|
|
|
|
|
service.db.one(
|
|
|
|
|
"SELECT payload FROM app_config WHERE key='wrist-thumbnail:living-room'"
|
|
|
|
|
)
|
|
|
|
|
is None
|
|
|
|
|
)
|
|
|
|
|
assert client.get(original["url"]).status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_recover_legacy_receipt_does_not_guess_normal_images(service):
|
|
|
|
|
from pigway_frame_flow.display.thumbnail_gallery import recover
|
|
|
|
|
from pigway_frame_flow.storage.database import encode
|
|
|
|
|
|
|
|
|
|
thumbnail = service.assets.put(b"fixture", "watch.jpg", "cache")
|
|
|
|
|
normal = service.assets.put(b"fixture", "watch.jpg", "images")
|
|
|
|
|
service.db.execute(
|
|
|
|
|
"INSERT INTO app_config VALUES(?,?)",
|
|
|
|
|
(
|
|
|
|
|
"wrist-thumbnail:living-room",
|
|
|
|
|
encode({**thumbnail, "width": 410, "height": 502}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
recover(service)
|
|
|
|
|
recover(service)
|
|
|
|
|
assert service.db.one(
|
|
|
|
|
"SELECT payload FROM asset_sources WHERE asset_id=?", (thumbnail["asset_id"],)
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
service.db.one(
|
|
|
|
|
"SELECT payload FROM asset_sources WHERE asset_id=?", (normal["asset_id"],)
|
|
|
|
|
)
|
|
|
|
|
is None
|
|
|
|
|
)
|