feat(core): asset & tag data-access
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
from photoflow.core.models import Asset, Tag
|
||||||
|
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 1
|
||||||
|
|
||||||
SCHEMA = """
|
SCHEMA = """
|
||||||
@@ -106,3 +108,74 @@ class Store:
|
|||||||
if self._conn is not None:
|
if self._conn is not None:
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
self._conn = None
|
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")]
|
||||||
|
|||||||
@@ -22,3 +22,60 @@ def test_meta_roundtrip_and_default(tmp_path):
|
|||||||
s.set_meta("last_ingest_at", "newer") # upsert
|
s.set_meta("last_ingest_at", "newer") # upsert
|
||||||
assert s.get_meta("last_ingest_at") == "newer"
|
assert s.get_meta("last_ingest_at") == "newer"
|
||||||
s.close()
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user