diff --git a/backend/app/database/images.py b/backend/app/database/images.py index dadbc202..4e10acc0 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -33,6 +33,10 @@ class ImageRecord(TypedDict, total=False): latitude: Optional[float] longitude: Optional[float] captured_at: Optional[datetime] + # New fields for video support + is_video: Optional[bool] + duration: Optional[float] + codec: Optional[str] class UntaggedImageRecord(TypedDict): @@ -70,6 +74,9 @@ def db_create_images_table() -> None: metadata TEXT, isTagged BOOLEAN DEFAULT 0, isFavourite BOOLEAN DEFAULT 0, + is_video BOOLEAN DEFAULT 0, + duration REAL, + codec TEXT, latitude REAL, longitude REAL, captured_at DATETIME, @@ -103,6 +110,17 @@ def db_create_images_table() -> None: """ ) + # Ensure new video-related columns exist for pre-existing databases + cursor.execute("PRAGMA table_info(images)") + existing_columns = {row[1] for row in cursor.fetchall()} + + if "is_video" not in existing_columns: + cursor.execute("ALTER TABLE images ADD COLUMN is_video BOOLEAN DEFAULT 0") + if "duration" not in existing_columns: + cursor.execute("ALTER TABLE images ADD COLUMN duration REAL") + if "codec" not in existing_columns: + cursor.execute("ALTER TABLE images ADD COLUMN codec TEXT") + conn.commit() conn.close() @@ -118,8 +136,36 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool: try: cursor.executemany( """ - INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged, latitude, longitude, captured_at) - VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged, :latitude, :longitude, :captured_at) + INSERT INTO images ( + id, + path, + folder_id, + thumbnailPath, + metadata, + isTagged, + isFavourite, + is_video, + duration, + codec, + latitude, + longitude, + captured_at + ) + VALUES ( + :id, + :path, + :folder_id, + :thumbnailPath, + :metadata, + :isTagged, + COALESCE(:isFavourite, 0), + COALESCE(:is_video, 0), + :duration, + :codec, + :latitude, + :longitude, + :captured_at + ) ON CONFLICT(path) DO UPDATE SET folder_id=excluded.folder_id, thumbnailPath=excluded.thumbnailPath, @@ -128,6 +174,10 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool: WHEN excluded.isTagged THEN 1 ELSE images.isTagged END, + isFavourite=COALESCE(excluded.isFavourite, images.isFavourite), + is_video=COALESCE(excluded.is_video, images.is_video), + duration=COALESCE(excluded.duration, images.duration), + codec=COALESCE(excluded.codec, images.codec), latitude=COALESCE(excluded.latitude, images.latitude), longitude=COALESCE(excluded.longitude, images.longitude), captured_at=COALESCE(excluded.captured_at, images.captured_at) @@ -169,6 +219,9 @@ def db_get_all_images(tagged: Union[bool, None] = None) -> List[dict]: i.metadata, i.isTagged, i.isFavourite, + i.is_video, + i.duration, + i.codec, i.latitude, i.longitude, i.captured_at, @@ -199,6 +252,9 @@ def db_get_all_images(tagged: Union[bool, None] = None) -> List[dict]: metadata, is_tagged, is_favourite, + is_video, + duration, + codec, latitude, longitude, captured_at, @@ -218,6 +274,9 @@ def db_get_all_images(tagged: Union[bool, None] = None) -> List[dict]: "metadata": metadata_dict, "isTagged": bool(is_tagged), "isFavourite": bool(is_favourite), + "is_video": bool(is_video), + "duration": duration, + "codec": codec, "latitude": latitude, "longitude": longitude, "captured_at": ( diff --git a/backend/app/routes/images.py b/backend/app/routes/images.py index 2e40cd82..0d230891 100644 --- a/backend/app/routes/images.py +++ b/backend/app/routes/images.py @@ -34,6 +34,9 @@ class ImageData(BaseModel): metadata: MetadataModel isTagged: bool isFavourite: bool + isVideo: bool = False + duration: Optional[float] = None + codec: Optional[str] = None tags: Optional[List[str]] = None @@ -66,6 +69,9 @@ def get_all_images( metadata=image_util_parse_metadata(image["metadata"]), isTagged=image["isTagged"], isFavourite=image.get("isFavourite", False), + isVideo=bool(image.get("is_video", False)), + duration=image.get("duration"), + codec=image.get("codec"), tags=image["tags"], ) for image in images diff --git a/backend/app/utils/images.py b/backend/app/utils/images.py index ccf65cdf..57f551dd 100644 --- a/backend/app/utils/images.py +++ b/backend/app/utils/images.py @@ -204,6 +204,11 @@ def image_util_prepare_image_records( "thumbnailPath": thumbnail_path, "metadata": metadata_json, "isTagged": False, + # Default video-related fields for image ingestion + "isFavourite": False, + "is_video": False, + "duration": None, + "codec": None, "latitude": latitude, # Can be None "longitude": longitude, # Can be None "captured_at": ( diff --git a/backend/tests/test_video_metadata_schema.py b/backend/tests/test_video_metadata_schema.py new file mode 100644 index 00000000..e5774703 --- /dev/null +++ b/backend/tests/test_video_metadata_schema.py @@ -0,0 +1,56 @@ +import sqlite3 +from pathlib import Path + +from app.database.images import db_create_images_table + + +def test_db_create_images_table_adds_video_columns(tmp_path, monkeypatch): + """ + Ensure db_create_images_table adds is_video, duration, and codec columns + to an existing images table that was created without them. + """ + + db_path = tmp_path / "test_video_schema.sqlite3" + + # Create an "old schema" images table without video columns + conn = sqlite3.connect(db_path) + try: + conn.execute( + """ + CREATE TABLE images ( + id TEXT PRIMARY KEY, + path VARCHAR UNIQUE, + folder_id INTEGER, + thumbnailPath TEXT UNIQUE, + metadata TEXT, + isTagged BOOLEAN DEFAULT 0, + isFavourite BOOLEAN DEFAULT 0, + latitude REAL, + longitude REAL, + captured_at DATETIME + ) + """ + ) + conn.commit() + finally: + conn.close() + + # Point the images module to this temporary database + monkeypatch.setattr("app.database.images.DATABASE_PATH", str(db_path)) + + # This should run CREATE TABLE IF NOT EXISTS (no-op on existing) + # and then ensure the new video-related columns exist via ALTER TABLE. + db_create_images_table() + + # Verify that the new columns are present + conn = sqlite3.connect(db_path) + try: + cursor = conn.execute("PRAGMA table_info(images)") + columns = {row[1] for row in cursor.fetchall()} + finally: + conn.close() + + assert "is_video" in columns + assert "duration" in columns + assert "codec" in columns +