diff --git a/shared/photoflow/core/store.py b/shared/photoflow/core/store.py index a7877d3..3cf889c 100644 --- a/shared/photoflow/core/store.py +++ b/shared/photoflow/core/store.py @@ -1,5 +1,7 @@ import sqlite3 +from photoflow.core.models import Asset, Tag + SCHEMA_VERSION = 1 SCHEMA = """ @@ -106,3 +108,74 @@ class Store: if self._conn is not None: self._conn.close() self._conn = None + + def upsert_asset(self, a: Asset) -> None: + has_gps = 1 if (a.gps_lat is not None and a.gps_lon is not None) else 0 + self.conn.execute( + """INSERT INTO assets + (immich_id, taken_at, gps_lat, gps_lon, place_city, place_country, + type, has_gps, thumb_path, processed, ingested_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(immich_id) DO UPDATE SET + taken_at=excluded.taken_at, gps_lat=excluded.gps_lat, + gps_lon=excluded.gps_lon, place_city=excluded.place_city, + place_country=excluded.place_country, type=excluded.type, + has_gps=excluded.has_gps, thumb_path=excluded.thumb_path, + ingested_at=excluded.ingested_at, updated_at=excluded.updated_at""", + (a.immich_id, a.taken_at, a.gps_lat, a.gps_lon, a.place_city, + a.place_country, a.type, has_gps, a.thumb_path, + 1 if a.processed else 0, a.ingested_at, a.updated_at)) + self.conn.commit() + + def _asset_from_row(self, r) -> Asset: + return Asset( + immich_id=r["immich_id"], taken_at=r["taken_at"], + gps_lat=r["gps_lat"], gps_lon=r["gps_lon"], + place_city=r["place_city"], place_country=r["place_country"], + type=r["type"], has_gps=bool(r["has_gps"]), thumb_path=r["thumb_path"], + processed=bool(r["processed"]), ingested_at=r["ingested_at"], + updated_at=r["updated_at"]) + + def get_asset(self, immich_id: str): + r = self.conn.execute("SELECT * FROM assets WHERE immich_id=?", (immich_id,)).fetchone() + return self._asset_from_row(r) if r else None + + def all_assets(self, include_processed: bool = True) -> list: + sql = "SELECT * FROM assets" + if not include_processed: + sql += " WHERE processed=0" + sql += " ORDER BY taken_at" + return [self._asset_from_row(r) for r in self.conn.execute(sql)] + + def assets_in_range(self, start_at: str, end_at: str) -> list: + return [self._asset_from_row(r) for r in self.conn.execute( + "SELECT * FROM assets WHERE taken_at>=? AND taken_at<=? ORDER BY taken_at", + (start_at, end_at))] + + def mark_processed(self, immich_id: str) -> None: + self.conn.execute("UPDATE assets SET processed=1 WHERE immich_id=?", (immich_id,)) + self.conn.commit() + + def set_asset_tags(self, immich_id: str, tag_names: list) -> None: + self.conn.execute("DELETE FROM asset_tags WHERE immich_id=?", (immich_id,)) + self.conn.executemany( + "INSERT OR IGNORE INTO asset_tags(immich_id, tag_name) VALUES(?, ?)", + [(immich_id, n) for n in tag_names]) + self.conn.commit() + + def asset_tags(self, immich_id: str) -> list: + return [r["tag_name"] for r in self.conn.execute( + "SELECT tag_name FROM asset_tags WHERE immich_id=? ORDER BY tag_name", + (immich_id,))] + + def upsert_tag(self, name: str, immich_tag_id=None, count: int = 0) -> None: + self.conn.execute( + """INSERT INTO tags(name, immich_tag_id, count) VALUES(?,?,?) + ON CONFLICT(name) DO UPDATE SET + immich_tag_id=excluded.immich_tag_id, count=excluded.count""", + (name, immich_tag_id, count)) + self.conn.commit() + + def all_tags(self) -> list: + return [Tag(name=r["name"], immich_tag_id=r["immich_tag_id"], count=r["count"]) + for r in self.conn.execute("SELECT * FROM tags ORDER BY name")] diff --git a/shared/tests/test_store_assets.py b/shared/tests/test_store_assets.py index f60a65b..09bedad 100644 --- a/shared/tests/test_store_assets.py +++ b/shared/tests/test_store_assets.py @@ -22,3 +22,60 @@ def test_meta_roundtrip_and_default(tmp_path): s.set_meta("last_ingest_at", "newer") # upsert assert s.get_meta("last_ingest_at") == "newer" s.close() + + +from photoflow.core.models import Asset + + +def _store(tmp_path): + return Store(str(tmp_path / "t.db")).connect() + + +def test_upsert_asset_roundtrip_and_has_gps(tmp_path): + s = _store(tmp_path) + s.upsert_asset(Asset(immich_id="a", taken_at="2019-06-01T10:00:00", + gps_lat=45.4, gps_lon=12.3, place_city="Venezia", type="IMAGE")) + got = s.get_asset("a") + assert got.place_city == "Venezia" and got.has_gps is True + s.upsert_asset(Asset(immich_id="b", taken_at="2019-06-02T10:00:00")) + assert s.get_asset("b").has_gps is False + s.close() + + +def test_upsert_preserves_processed(tmp_path): + s = _store(tmp_path) + s.upsert_asset(Asset(immich_id="a", taken_at="t")) + s.mark_processed("a") + s.upsert_asset(Asset(immich_id="a", taken_at="t2")) # re-ingest + got = s.get_asset("a") + assert got.processed is True and got.taken_at == "t2" + s.close() + + +def test_all_assets_ordered_and_range(tmp_path): + s = _store(tmp_path) + for i, t in [("c", "2019-06-03"), ("a", "2019-06-01"), ("b", "2019-06-02")]: + s.upsert_asset(Asset(immich_id=i, taken_at=t)) + assert [a.immich_id for a in s.all_assets()] == ["a", "b", "c"] + rng = s.assets_in_range("2019-06-02", "2019-06-03") + assert [a.immich_id for a in rng] == ["b", "c"] + s.close() + + +def test_asset_tags_replace(tmp_path): + s = _store(tmp_path) + s.upsert_asset(Asset(immich_id="a", taken_at="t")) + s.set_asset_tags("a", ["Italy 2019", "Kiev"]) + assert sorted(s.asset_tags("a")) == ["Italy 2019", "Kiev"] + s.set_asset_tags("a", ["Italy 2019"]) # replace + assert s.asset_tags("a") == ["Italy 2019"] + s.close() + + +def test_tags_inventory(tmp_path): + s = _store(tmp_path) + s.upsert_tag("Italy 2019", immich_tag_id="t1", count=42) + s.upsert_tag("Italy 2019", immich_tag_id="t1", count=43) # upsert + names = {t.name: t for t in s.all_tags()} + assert names["Italy 2019"].count == 43 and names["Italy 2019"].immich_tag_id == "t1" + s.close()