diff --git a/backend/app/database/albums.py b/backend/app/database/albums.py index b9e5b149a..db4f1e1f0 100644 --- a/backend/app/database/albums.py +++ b/backend/app/database/albums.py @@ -1,13 +1,9 @@ -import sqlite3 import bcrypt -from app.config.settings import DATABASE_PATH from app.database.connection import get_db_connection def db_create_albums_table() -> None: - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -20,16 +16,10 @@ def db_create_albums_table() -> None: ) """ ) - conn.commit() - finally: - if conn is not None: - conn.close() def db_create_album_images_table() -> None: - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -42,46 +32,33 @@ def db_create_album_images_table() -> None: ) """ ) - conn.commit() - finally: - if conn is not None: - conn.close() def db_get_all_albums(show_hidden: bool = False): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() if show_hidden: cursor.execute("SELECT * FROM albums") else: cursor.execute("SELECT * FROM albums WHERE is_hidden = 0") albums = cursor.fetchall() return albums - finally: - conn.close() def db_get_album_by_name(name: str): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute("SELECT * FROM albums WHERE album_name = ?", (name,)) album = cursor.fetchone() return album if album else None - finally: - conn.close() def db_get_album(album_id: str): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute("SELECT * FROM albums WHERE album_id = ?", (album_id,)) album = cursor.fetchone() return album if album else None - finally: - conn.close() def db_insert_album( @@ -91,9 +68,8 @@ def db_insert_album( is_hidden: bool = False, password: str = None, ): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() password_hash = None if password: password_hash = bcrypt.hashpw( @@ -106,9 +82,6 @@ def db_insert_album( """, (album_id, album_name, description, int(is_hidden), password_hash), ) - conn.commit() - finally: - conn.close() def db_update_album( @@ -118,9 +91,8 @@ def db_update_album( is_hidden: bool, password: str = None, ): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() if password is not None: # Update with new password password_hash = bcrypt.hashpw( @@ -144,9 +116,6 @@ def db_update_album( """, (album_name, description, int(is_hidden), album_id), ) - conn.commit() - finally: - conn.close() def db_delete_album(album_id: str): @@ -156,16 +125,13 @@ def db_delete_album(album_id: str): def db_get_album_images(album_id: str): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( "SELECT image_id FROM album_images WHERE album_id = ?", (album_id,) ) images = cursor.fetchall() return [img[0] for img in images] - finally: - conn.close() def db_add_images_to_album(album_id: str, image_ids: list[str]): @@ -207,22 +173,17 @@ def db_remove_image_from_album(album_id: str, image_id: str): def db_remove_images_from_album(album_id: str, image_ids: list[str]): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.executemany( "DELETE FROM album_images WHERE album_id = ? AND image_id = ?", [(album_id, img_id) for img_id in image_ids], ) - conn.commit() - finally: - conn.close() def verify_album_password(album_id: str, password: str) -> bool: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( "SELECT password_hash FROM albums WHERE album_id = ?", (album_id,) ) @@ -230,5 +191,3 @@ def verify_album_password(album_id: str, password: str) -> bool: if not row or not row[0]: return False return bcrypt.checkpw(password.encode("utf-8"), row[0].encode("utf-8")) - finally: - conn.close() diff --git a/backend/app/database/connection.py b/backend/app/database/connection.py index 599526dc1..cb6a1b72c 100644 --- a/backend/app/database/connection.py +++ b/backend/app/database/connection.py @@ -2,6 +2,9 @@ from contextlib import contextmanager from typing import Generator from app.config.settings import DATABASE_PATH +from app.logging.setup_logging import get_logger + +logger = get_logger(__name__) @contextmanager @@ -21,12 +24,14 @@ def get_db_connection() -> Generator[sqlite3.Connection, None, None]: conn.execute("PRAGMA recursive_triggers = ON;") # Allow nested triggers conn.execute("PRAGMA defer_foreign_keys = OFF;") # Immediate FK checking conn.execute("PRAGMA case_sensitive_like = ON;") # Make LIKE case-sensitive - + conn.execute("PRAGMA journal_mode=WAL;") + conn.execute("PRAGMA busy_timeout=30000;") # Wait for 30s if DB locked try: yield conn conn.commit() except Exception: conn.rollback() + logger.exception("Database transaction failed, rolled back.") raise finally: conn.close() diff --git a/backend/app/database/face_clusters.py b/backend/app/database/face_clusters.py index ceac7f556..538928191 100644 --- a/backend/app/database/face_clusters.py +++ b/backend/app/database/face_clusters.py @@ -1,6 +1,6 @@ import sqlite3 from typing import Optional, List, Dict, TypedDict, Union -from app.config.settings import DATABASE_PATH +from app.database.connection import get_db_connection # Type definitions ClusterId = str @@ -20,9 +20,7 @@ class ClusterData(TypedDict): def db_create_clusters_table() -> None: """Create the face_clusters table if it doesn't exist.""" - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -33,52 +31,41 @@ def db_create_clusters_table() -> None: ) """ ) - conn.commit() - finally: - if conn is not None: - conn.close() -def db_delete_all_clusters(cursor: Optional[sqlite3.Cursor] = None) -> int: +def db_delete_all_clusters(conn: Optional[sqlite3.Connection] = None) -> int: """ Delete all clusters from the database. Args: - cursor: Optional existing database cursor. If None, creates a new connection. + conn: Optional existing database connection. If None, creates a new connection. Returns: Number of deleted clusters """ - own_connection = cursor is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + def db_delete_operation(cursor: sqlite3.Cursor) -> int: cursor.execute("DELETE FROM face_clusters") - deleted_count = cursor.rowcount - if own_connection: - conn.commit() - return deleted_count - except Exception: - if own_connection: - conn.rollback() - print("Error deleting all clusters.") - raise - finally: - if own_connection: - conn.close() + return cursor.rowcount + + if conn: + cursor = conn.cursor() + return db_delete_operation(cursor) + else: + with get_db_connection() as conn: + cursor = conn.cursor() + return db_delete_operation(cursor) def db_insert_clusters_batch( - clusters: List[ClusterData], cursor: Optional[sqlite3.Cursor] = None + clusters: List[ClusterData], conn: Optional[sqlite3.Connection] = None ) -> List[ClusterId]: """ Insert multiple clusters into the database in batch. Args: clusters: List of ClusterData objects containing cluster information. - cursor: Optional existing database cursor. If None, creates a new connection. + conn: Optional existing database connection. If None, creates a new connection. Returns: List of cluster IDs of the newly created clusters @@ -86,12 +73,7 @@ def db_insert_clusters_batch( if not clusters: return [] - own_connection = cursor is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - try: + def db_operation(cursor: sqlite3.Cursor) -> List[ClusterId]: cluster_ids = [] insert_data = [] @@ -111,16 +93,15 @@ def db_insert_clusters_batch( insert_data, ) - if own_connection: - conn.commit() return cluster_ids - except Exception: - if own_connection: - conn.rollback() - raise - finally: - if own_connection: - conn.close() + + if conn: + cursor = conn.cursor() + return db_operation(cursor) + else: + with get_db_connection() as conn: + cursor = conn.cursor() + return db_operation(cursor) def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]: @@ -133,10 +114,9 @@ def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]: Returns: ClusterData if found, None otherwise """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters WHERE cluster_id = ?", (cluster_id,), @@ -149,8 +129,6 @@ def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]: cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2] ) return None - finally: - conn.close() def db_get_all_clusters() -> List[ClusterData]: @@ -160,10 +138,9 @@ def db_get_all_clusters() -> List[ClusterData]: Returns: List of ClusterData objects """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters ORDER BY cluster_id" ) @@ -179,8 +156,6 @@ def db_get_all_clusters() -> List[ClusterData]: ) return clusters - finally: - conn.close() def db_update_cluster( @@ -199,14 +174,8 @@ def db_update_cluster( Returns: True if the cluster was updated, False if not found """ - # Use provided connection or create a new one - own_connection = conn is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - - cursor = conn.cursor() - try: + def db_update_operation(cursor: sqlite3.Cursor) -> bool: # Build the update query dynamically based on provided parameters update_fields = [] update_values = [] @@ -225,11 +194,15 @@ def db_update_cluster( update_values, ) - updated = cursor.rowcount > 0 - conn.commit() - return updated - finally: - conn.close() + return cursor.rowcount > 0 + + if conn: + cursor = conn.cursor() + return db_update_operation(cursor) + else: + with get_db_connection() as conn: + cursor = conn.cursor() + return db_update_operation(cursor) def db_get_all_clusters_with_face_counts() -> ( @@ -241,10 +214,8 @@ def db_get_all_clusters_with_face_counts() -> ( Returns: List of dictionaries containing cluster_id, cluster_name, face_count, and face_image_base64 """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( """ SELECT @@ -274,8 +245,6 @@ def db_get_all_clusters_with_face_counts() -> ( ) return clusters - finally: - conn.close() def db_get_images_by_cluster_id( @@ -290,10 +259,8 @@ def db_get_images_by_cluster_id( Returns: List of dictionaries containing image data with face information """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( """ SELECT DISTINCT @@ -347,5 +314,3 @@ def db_get_images_by_cluster_id( ) return images - finally: - conn.close() diff --git a/backend/app/database/faces.py b/backend/app/database/faces.py index 0e43f7117..ec5fdeadd 100644 --- a/backend/app/database/faces.py +++ b/backend/app/database/faces.py @@ -2,7 +2,7 @@ import json import numpy as np from typing import Optional, List, Dict, Union, TypedDict -from app.config.settings import DATABASE_PATH +from app.database.connection import get_db_connection # Type definitions FaceId = int @@ -27,10 +27,9 @@ class FaceData(TypedDict): def db_create_faces_table() -> None: - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) - conn.execute("PRAGMA foreign_keys = ON") + """Create the faces table if it doesn't exist.""" + + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -46,10 +45,6 @@ def db_create_faces_table() -> None: ) """ ) - conn.commit() - finally: - if conn is not None: - conn.close() def db_insert_face_embeddings( @@ -70,10 +65,9 @@ def db_insert_face_embeddings( bbox: Bounding box coordinates as dict with keys: x, y, width, height (optional) cluster_id: ID of the face cluster this face belongs to (optional) """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: embeddings_json = json.dumps([emb.tolist() for emb in embeddings]) # Convert bbox to JSON string if provided @@ -88,10 +82,7 @@ def db_insert_face_embeddings( ) face_id = cursor.lastrowid - conn.commit() return face_id - finally: - conn.close() def db_insert_face_embeddings_by_image_id( @@ -142,10 +133,9 @@ def db_insert_face_embeddings_by_image_id( def get_all_face_embeddings(): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( """ SELECT @@ -212,8 +202,6 @@ def get_all_face_embeddings(): # Sort by path images.sort(key=lambda x: x["path"]) return images - finally: - conn.close() def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbedding]]]: @@ -223,10 +211,9 @@ def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbed Returns: List of dictionaries containing face_id and embeddings (as numpy array) """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute("SELECT face_id, embeddings FROM faces WHERE cluster_id IS NULL") rows = cursor.fetchall() @@ -239,8 +226,6 @@ def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbed faces.append({"face_id": face_id, "embeddings": embeddings}) return faces - finally: - conn.close() def db_get_all_faces_with_cluster_names() -> ( @@ -252,10 +237,9 @@ def db_get_all_faces_with_cluster_names() -> ( Returns: List of dictionaries containing face_id, embeddings (as numpy array), and cluster_name """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( """ SELECT f.face_id, f.embeddings, fc.cluster_name @@ -281,13 +265,11 @@ def db_get_all_faces_with_cluster_names() -> ( ) return faces - finally: - conn.close() def db_update_face_cluster_ids_batch( face_cluster_mapping: List[Dict[str, Union[FaceId, ClusterId]]], - cursor: Optional[sqlite3.Cursor] = None, + conn: Optional[sqlite3.Connection] = None, ) -> None: """ Update cluster IDs for multiple faces in batch. @@ -295,7 +277,7 @@ def db_update_face_cluster_ids_batch( Args: face_cluster_mapping: List of dictionaries containing face_id and cluster_id pairs Each dict should have keys: 'face_id' and 'cluster_id' - cursor: Optional existing database cursor. If None, creates a new connection. + conn: Optional existing database connection. If None, creates a new connection. Example: face_cluster_mapping = [ @@ -307,12 +289,7 @@ def db_update_face_cluster_ids_batch( if not face_cluster_mapping: return - own_connection = cursor is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - try: + def _update(cursor: sqlite3.Cursor) -> None: # Prepare update data as tuples (cluster_id, face_id) update_data = [] for mapping in face_cluster_mapping: @@ -329,16 +306,11 @@ def db_update_face_cluster_ids_batch( update_data, ) - if own_connection: - conn.commit() - except Exception: - if own_connection: - conn.rollback() - print("Error updating face cluster IDs in batch.") - raise - finally: - if own_connection: - conn.close() + if conn: + _update(conn.cursor()) + else: + with get_db_connection() as conn: + _update(conn.cursor()) def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding]]]: @@ -349,10 +321,9 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding] List of dictionaries containing cluster_id and mean_embedding (as numpy array) Only returns clusters that have at least one face assigned """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( """ SELECT f.cluster_id, f.embeddings @@ -390,5 +361,3 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding] ) return cluster_means - finally: - conn.close() diff --git a/backend/app/database/folders.py b/backend/app/database/folders.py index 3a2ac976d..8f9ad5620 100644 --- a/backend/app/database/folders.py +++ b/backend/app/database/folders.py @@ -1,8 +1,7 @@ -import sqlite3 import os import uuid from typing import List, Tuple, Dict, Optional -from app.config.settings import DATABASE_PATH +from app.database.connection import get_db_connection # Type definitions FolderId = str @@ -13,9 +12,7 @@ def db_create_folders_table() -> None: - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -30,10 +27,6 @@ def db_create_folders_table() -> None: ) """ ) - conn.commit() - finally: - if conn is not None: - conn.close() def db_insert_folders_batch(folders_data: List[FolderData]) -> None: @@ -42,20 +35,13 @@ def db_insert_folders_batch(folders_data: List[FolderData]) -> None: folders_data: list of tuples (folder_id, folder_path, parent_folder_id,last_modified_time, AI_Tagging, taggingCompleted) """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.executemany( """INSERT OR IGNORE INTO folders (folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, taggingCompleted) VALUES (?, ?, ?, ?, ?, ?)""", folders_data, ) - conn.commit() - except Exception as e: - conn.rollback() - raise e - finally: - conn.close() def db_insert_folder( @@ -65,10 +51,9 @@ def db_insert_folder( taggingCompleted: Optional[bool] = None, folder_id: Optional[FolderId] = None, ) -> FolderId: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: abs_folder_path = os.path.abspath(folder_path) if not os.path.isdir(abs_folder_path): raise ValueError(f"Error: '{folder_path}' is not a valid directory.") @@ -100,16 +85,12 @@ def db_insert_folder( ), ) - conn.commit() return folder_id - finally: - conn.close() def db_get_folder_id_from_path(folder_path: FolderPath) -> Optional[FolderId]: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() abs_folder_path = os.path.abspath(folder_path) cursor.execute( "SELECT folder_id FROM folders WHERE folder_path = ?", @@ -117,39 +98,31 @@ def db_get_folder_id_from_path(folder_path: FolderPath) -> Optional[FolderId]: ) result = cursor.fetchone() return result[0] if result else None - finally: - conn.close() def db_get_folder_path_from_id(folder_id: FolderId) -> Optional[FolderPath]: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( "SELECT folder_path FROM folders WHERE folder_id = ?", (folder_id,), ) result = cursor.fetchone() return result[0] if result else None - finally: - conn.close() def db_get_all_folders() -> List[FolderPath]: - with sqlite3.connect(DATABASE_PATH) as conn: + with get_db_connection() as conn: rows = conn.execute("SELECT folder_path FROM folders").fetchall() return [row[0] for row in rows] if rows else [] def db_get_all_folder_ids() -> List[FolderId]: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute("SELECT folder_id from folders") rows = cursor.fetchall() return [row[0] for row in rows] if rows else [] - finally: - conn.close() def db_delete_folders_batch(folder_ids: List[FolderId]) -> int: @@ -161,13 +134,11 @@ def db_delete_folders_batch(folder_ids: List[FolderId]) -> int: if not folder_ids: return 0 - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: # Enable foreign keys for cascading deletes cursor.execute("PRAGMA foreign_keys = ON;") - conn.commit() # Create placeholders for the IN clause placeholders = ",".join("?" * len(folder_ids)) @@ -178,19 +149,12 @@ def db_delete_folders_batch(folder_ids: List[FolderId]) -> int: ) deleted_count = cursor.rowcount - conn.commit() return deleted_count - except Exception as e: - conn.rollback() - raise e - finally: - conn.close() def db_delete_folder(folder_path: FolderPath) -> None: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() abs_folder_path = os.path.abspath(folder_path) cursor.execute( "PRAGMA foreign_keys = ON;" @@ -212,10 +176,6 @@ def db_delete_folder(folder_path: FolderPath) -> None: (abs_folder_path,), ) - conn.commit() - finally: - conn.close() - def db_update_parent_ids_for_subtree( root_folder_path: FolderPath, folder_map: FolderMap @@ -225,9 +185,8 @@ def db_update_parent_ids_for_subtree( Only updates folders whose parent_folder_id is NULL. folder_map: dict mapping folder_path to tuple of (folder_id, parent_id) """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() for folder_path, (folder_id, parent_id) in folder_map.items(): if parent_id: cursor.execute( @@ -238,9 +197,6 @@ def db_update_parent_ids_for_subtree( """, (parent_id, folder_path), ) - conn.commit() - finally: - conn.close() def db_folder_exists(folder_path: FolderPath) -> bool: @@ -248,17 +204,14 @@ def db_folder_exists(folder_path: FolderPath) -> bool: Check if a folder exists in the database. Returns True if the folder exists, False otherwise. """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() abs_path = os.path.abspath(folder_path) cursor.execute( "SELECT folder_id FROM folders WHERE folder_path = ?", (abs_path,) ) result = cursor.fetchone() return bool(result) - finally: - conn.close() def db_find_parent_folder_id(folder_path: FolderPath) -> Optional[FolderId]: @@ -270,16 +223,13 @@ def db_find_parent_folder_id(folder_path: FolderPath) -> Optional[FolderId]: if not parent_path or parent_path == folder_path: # Root directory return None - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( "SELECT folder_id FROM folders WHERE folder_path = ?", (parent_path,) ) result = cursor.fetchone() return result[0] if result else None - finally: - conn.close() def db_update_ai_tagging_batch( @@ -294,10 +244,9 @@ def db_update_ai_tagging_batch( if not folder_ids: return 0 - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: # Create placeholders for the IN clause placeholders = ",".join("?" * len(folder_ids)) @@ -307,13 +256,7 @@ def db_update_ai_tagging_batch( ) updated_count = cursor.rowcount - conn.commit() return updated_count - except Exception as e: - conn.rollback() - raise e - finally: - conn.close() def db_enable_ai_tagging_batch(folder_ids: List[FolderId]) -> int: @@ -336,10 +279,9 @@ def db_disable_ai_tagging_batch(folder_ids: List[FolderId]) -> int: def db_get_folder_ids_by_path_prefix(root_path: str) -> List[FolderIdPath]: """Get all folder IDs and paths whose path starts with the given root path.""" - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: # Use path LIKE with wildcard to match all subfolders cursor.execute( """ @@ -350,8 +292,6 @@ def db_get_folder_ids_by_path_prefix(root_path: str) -> List[FolderIdPath]: ) return cursor.fetchall() - finally: - conn.close() def db_get_folder_ids_by_paths( @@ -369,10 +309,9 @@ def db_get_folder_ids_by_paths( if not folder_paths: return {} - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: # Convert all paths to absolute paths abs_paths = [os.path.abspath(path) for path in folder_paths] @@ -390,8 +329,6 @@ def db_get_folder_ids_by_paths( path_to_id = {folder_path: folder_id for folder_path, folder_id in results} return path_to_id - finally: - conn.close() def db_get_all_folder_details() -> ( @@ -402,10 +339,8 @@ def db_get_all_folder_details() -> ( last_modified_time, AI_Tagging, and taggingCompleted. Returns list of tuples with all folder information. """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( """ SELECT folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, taggingCompleted @@ -414,8 +349,6 @@ def db_get_all_folder_details() -> ( """ ) return cursor.fetchall() - finally: - conn.close() def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]: @@ -423,10 +356,9 @@ def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]: Get all direct child folders (not subfolders) for a given parent folder. Returns list of tuples (folder_id, folder_path). """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute( """ SELECT folder_id, folder_path FROM folders @@ -436,5 +368,3 @@ def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]: ) return cursor.fetchall() - finally: - conn.close() diff --git a/backend/app/database/images.py b/backend/app/database/images.py index ec9541a56..e4b3ccd0e 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -1,12 +1,9 @@ # Standard library imports -import sqlite3 from typing import Any, List, Mapping, Tuple, TypedDict, Union # App-specific imports -from app.config.settings import ( - DATABASE_PATH, -) from app.logging.setup_logging import get_logger +from app.database.connection import get_db_connection # Initialize logger logger = get_logger(__name__) @@ -42,48 +39,39 @@ class UntaggedImageRecord(TypedDict): ImageClassPair = Tuple[ImageId, ClassId] -def _connect() -> sqlite3.Connection: - conn = sqlite3.connect(DATABASE_PATH) - # Ensure ON DELETE CASCADE and other FKs are enforced - conn.execute("PRAGMA foreign_keys = ON") - return conn - - def db_create_images_table() -> None: - conn = _connect() - cursor = conn.cursor() + """Create the images and image_classes tables if they don't exist.""" - # Create new images table with merged fields - cursor.execute( + with get_db_connection() as conn: + cursor = conn.cursor() + + # Create new images table with merged fields + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS images ( + id TEXT PRIMARY KEY, + path VARCHAR UNIQUE, + folder_id INTEGER, + thumbnailPath TEXT UNIQUE, + metadata TEXT, + isTagged BOOLEAN DEFAULT 0, + FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + ) """ - CREATE TABLE IF NOT EXISTS images ( - id TEXT PRIMARY KEY, - path VARCHAR UNIQUE, - folder_id INTEGER, - thumbnailPath TEXT UNIQUE, - metadata TEXT, - isTagged BOOLEAN DEFAULT 0, - isFavourite BOOLEAN DEFAULT 0, - FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE ) - """ - ) - # Create new image_classes junction table - cursor.execute( + # Create new image_classes junction table + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS image_classes ( + image_id TEXT, + class_id INTEGER, + PRIMARY KEY (image_id, class_id), + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, + FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE + ) """ - CREATE TABLE IF NOT EXISTS image_classes ( - image_id TEXT, - class_id INTEGER, - PRIMARY KEY (image_id, class_id), - FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, - FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE ) - """ - ) - - conn.commit() - conn.close() def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool: @@ -91,33 +79,29 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool: if not image_records: return True - conn = _connect() - cursor = conn.cursor() - try: - cursor.executemany( - """ - INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged) - VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged) - ON CONFLICT(path) DO UPDATE SET - folder_id=excluded.folder_id, - thumbnailPath=excluded.thumbnailPath, - metadata=excluded.metadata, - isTagged=CASE - WHEN excluded.isTagged THEN 1 - ELSE images.isTagged - END - """, - image_records, - ) - conn.commit() - return True + with get_db_connection() as conn: + cursor = conn.cursor() + + cursor.executemany( + """ + INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged) + VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged) + ON CONFLICT(path) DO UPDATE SET + folder_id=excluded.folder_id, + thumbnailPath=excluded.thumbnailPath, + metadata=excluded.metadata, + isTagged=CASE + WHEN excluded.isTagged THEN 1 + ELSE images.isTagged + END + """, + image_records, + ) + return True except Exception as e: logger.error(f"Error inserting image records: {e}") - conn.rollback() return False - finally: - conn.close() def db_get_all_images(tagged: Union[bool, None] = None) -> List[dict]: @@ -131,87 +115,81 @@ def db_get_all_images(tagged: Union[bool, None] = None) -> List[dict]: Returns: List of dictionaries containing all image data including tags """ - conn = _connect() - cursor = conn.cursor() - try: - # Build the query with optional WHERE clause - query = """ - SELECT - i.id, - i.path, - i.folder_id, - i.thumbnailPath, - i.metadata, - i.isTagged, - i.isFavourite, - m.name as tag_name - FROM images i - LEFT JOIN image_classes ic ON i.id = ic.image_id - LEFT JOIN mappings m ON ic.class_id = m.class_id - """ - - params = [] - if tagged is not None: - query += " WHERE i.isTagged = ?" - params.append(tagged) - - query += " ORDER BY i.path, m.name" - - cursor.execute(query, params) - - results = cursor.fetchall() - - # Group results by image ID - images_dict = {} - for ( - image_id, - path, - folder_id, - thumbnail_path, - metadata, - is_tagged, - is_favourite, - tag_name, - ) in results: - if image_id not in images_dict: - # Safely parse metadata JSON -> dict - from app.utils.images import image_util_parse_metadata - - metadata_dict = image_util_parse_metadata(metadata) - - images_dict[image_id] = { - "id": image_id, - "path": path, - "folder_id": str(folder_id), - "thumbnailPath": thumbnail_path, - "metadata": metadata_dict, - "isTagged": bool(is_tagged), - "isFavourite": bool(is_favourite), - "tags": [], - } - - # Add tag if it exists (avoid duplicates) - if tag_name and tag_name not in images_dict[image_id]["tags"]: - images_dict[image_id]["tags"].append(tag_name) - - # Convert to list and set tags to None if empty - images = [] - for image_data in images_dict.values(): - if not image_data["tags"]: - image_data["tags"] = None - images.append(image_data) - - # Sort by path - images.sort(key=lambda x: x["path"]) - - return images + with get_db_connection() as conn: + cursor = conn.cursor() + + # Build the query with optional WHERE clause + query = """ + SELECT + i.id, + i.path, + i.folder_id, + i.thumbnailPath, + i.metadata, + i.isTagged, + m.name as tag_name + FROM images i + LEFT JOIN image_classes ic ON i.id = ic.image_id + LEFT JOIN mappings m ON ic.class_id = m.class_id + """ + params = [] + if tagged is not None: + query += " WHERE i.isTagged = ?" + params.append(tagged) + + query += " ORDER BY i.path, m.name" + + cursor.execute(query, params) + + results = cursor.fetchall() + + # Group results by image ID + images_dict = {} + for ( + image_id, + path, + folder_id, + thumbnail_path, + metadata, + is_tagged, + tag_name, + ) in results: + if image_id not in images_dict: + # Safely parse metadata JSON -> dict + from app.utils.images import image_util_parse_metadata + + metadata_dict = image_util_parse_metadata(metadata) + + images_dict[image_id] = { + "id": image_id, + "path": path, + "folder_id": str(folder_id), + "thumbnailPath": thumbnail_path, + "metadata": metadata_dict, + "isTagged": bool(is_tagged), + "tags": [], + } + + # Add tag if it exists (avoid duplicates) + if tag_name and tag_name not in images_dict[image_id]["tags"]: + images_dict[image_id]["tags"].append(tag_name) + + # Convert to list and set tags to None if empty + images = [] + for image_data in images_dict.values(): + if not image_data["tags"]: + image_data["tags"] = None + images.append(image_data) + + # Sort by path + images.sort(key=lambda x: x["path"]) + + return images except Exception as e: logger.error(f"Error getting all images: {e}") return [] - finally: - conn.close() def db_get_untagged_images() -> List[UntaggedImageRecord]: @@ -224,10 +202,8 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: Returns: List of dictionaries containing image data: id, path, folder_id, thumbnailPath, metadata """ - conn = _connect() - cursor = conn.cursor() - - try: + with get_db_connection() as conn: + cursor = conn.cursor() cursor.execute( """ SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata @@ -257,9 +233,6 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: return untagged_images - finally: - conn.close() - def db_update_image_tagged_status(image_id: ImageId, is_tagged: bool = True) -> bool: """ @@ -272,22 +245,18 @@ def db_update_image_tagged_status(image_id: ImageId, is_tagged: bool = True) -> Returns: True if update was successful, False otherwise """ - conn = _connect() - cursor = conn.cursor() - try: - cursor.execute( - "UPDATE images SET isTagged = ? WHERE id = ?", - (is_tagged, image_id), - ) - conn.commit() - return cursor.rowcount > 0 + with get_db_connection() as conn: + cursor = conn.cursor() + + cursor.execute( + "UPDATE images SET isTagged = ? WHERE id = ?", + (is_tagged, image_id), + ) + return cursor.rowcount > 0 except Exception as e: logger.error(f"Error updating image tagged status: {e}") - conn.rollback() return False - finally: - conn.close() def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bool: @@ -303,25 +272,20 @@ def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bo if not image_class_pairs: return True - conn = _connect() - cursor = conn.cursor() - try: - cursor.executemany( - """ - INSERT OR IGNORE INTO image_classes (image_id, class_id) - VALUES (?, ?) - """, - image_class_pairs, - ) - conn.commit() - return True + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.executemany( + """ + INSERT OR IGNORE INTO image_classes (image_id, class_id) + VALUES (?, ?) + """, + image_class_pairs, + ) + return True except Exception as e: logger.error(f"Error inserting image classes: {e}") - conn.rollback() return False - finally: - conn.close() def db_get_images_by_folder_ids( @@ -339,26 +303,23 @@ def db_get_images_by_folder_ids( if not folder_ids: return [] - conn = _connect() - cursor = conn.cursor() - try: - # Create placeholders for the IN clause - placeholders = ",".join("?" for _ in folder_ids) - cursor.execute( - f""" - SELECT id, path, thumbnailPath - FROM images - WHERE folder_id IN ({placeholders}) - """, - folder_ids, - ) - return cursor.fetchall() + with get_db_connection() as conn: + cursor = conn.cursor() + # Create placeholders for the IN clause + placeholders = ",".join("?" for _ in folder_ids) + cursor.execute( + f""" + SELECT id, path, thumbnailPath + FROM images + WHERE folder_id IN ({placeholders}) + """, + folder_ids, + ) + return cursor.fetchall() except Exception as e: logger.error(f"Error getting images by folder IDs: {e}") return [] - finally: - conn.close() def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool: @@ -375,47 +336,43 @@ def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool: if not image_ids: return True - conn = _connect() - cursor = conn.cursor() - try: - # Create placeholders for the IN clause - placeholders = ",".join("?" for _ in image_ids) - cursor.execute( - f"DELETE FROM images WHERE id IN ({placeholders})", - image_ids, - ) - conn.commit() - logger.info(f"Deleted {cursor.rowcount} obsolete image(s) from database") - return True + with get_db_connection() as conn: + cursor = conn.cursor() + # Create placeholders for the IN clause + placeholders = ",".join("?" for _ in image_ids) + cursor.execute( + f"DELETE FROM images WHERE id IN ({placeholders})", + image_ids, + ) + logger.info(f"Deleted {cursor.rowcount} obsolete image(s) from database") + return True except Exception as e: logger.error(f"Error deleting images: {e}") - conn.rollback() return False - finally: - conn.close() def db_toggle_image_favourite_status(image_id: str) -> bool: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() try: - cursor.execute("SELECT id FROM images WHERE id = ?", (image_id,)) - if not cursor.fetchone(): - return False - cursor.execute( - """ - UPDATE images - SET isFavourite = CASE WHEN isFavourite = 1 THEN 0 ELSE 1 END - WHERE id = ? - """, - (image_id,), - ) - conn.commit() - return cursor.rowcount > 0 + with get_db_connection() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT id FROM images WHERE id = ?", (image_id,)) + if not cursor.fetchone(): + return False + + cursor.execute( + """ + UPDATE images + SET isFavourite = CASE WHEN isFavourite = 1 THEN 0 ELSE 1 END + WHERE id = ? + """, + (image_id,), + ) + + conn.commit() + return cursor.rowcount > 0 + except Exception as e: logger.error(f"Database error: {e}") - conn.rollback() return False - finally: - conn.close() diff --git a/backend/app/database/metadata.py b/backend/app/database/metadata.py index d431f6e2b..c94703c9d 100644 --- a/backend/app/database/metadata.py +++ b/backend/app/database/metadata.py @@ -2,14 +2,13 @@ import sqlite3 import json from typing import Optional, Dict, Any -from app.config.settings import DATABASE_PATH +from app.database.connection import get_db_connection def db_create_metadata_table() -> None: """Create the metadata table if it doesn't exist.""" - conn = None - try: - conn = sqlite3.connect(DATABASE_PATH) + + with get_db_connection() as conn: cursor = conn.cursor() cursor.execute( """ @@ -24,11 +23,6 @@ def db_create_metadata_table() -> None: if cursor.fetchone()[0] == 0: cursor.execute("INSERT INTO metadata (metadata) VALUES (?)", ("{}",)) - conn.commit() - finally: - if conn is not None: - conn.close() - def db_get_metadata() -> Optional[Dict[str, Any]]: """ @@ -37,10 +31,9 @@ def db_get_metadata() -> Optional[Dict[str, Any]]: Returns: Dictionary containing metadata, or None if not found """ - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() + with get_db_connection() as conn: + cursor = conn.cursor() - try: cursor.execute("SELECT metadata FROM metadata LIMIT 1") row = cursor.fetchone() @@ -51,45 +44,32 @@ def db_get_metadata() -> Optional[Dict[str, Any]]: except json.JSONDecodeError: return None return None - finally: - conn.close() def db_update_metadata( - metadata: Dict[str, Any], cursor: Optional[sqlite3.Cursor] = None + metadata: Dict[str, Any], conn: Optional[sqlite3.Connection] = None ) -> bool: """ Update the metadata in the database. Args: metadata: Dictionary containing metadata to store - cursor: Optional existing database cursor. If None, creates a new connection. + conn: Optional existing database connection. If None, creates a new connection. Returns: True if the metadata was updated, False otherwise """ - own_connection = cursor is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + def _update(cursor: sqlite3.Cursor) -> bool: metadata_json = json.dumps(metadata) - - # Delete all existing rows and insert new one cursor.execute("DELETE FROM metadata") cursor.execute("INSERT INTO metadata (metadata) VALUES (?)", (metadata_json,)) + return cursor.rowcount > 0 - success = cursor.rowcount > 0 - if own_connection: - conn.commit() - return success - except Exception as e: - if own_connection: - conn.rollback() - - print(f"Error updating metadata: {e}") - raise - finally: - if own_connection: - conn.close() + if conn: + cursor = conn.cursor() + return _update(cursor) + else: + with get_db_connection() as conn: + cursor = conn.cursor() + return _update(cursor) diff --git a/backend/app/utils/face_clusters.py b/backend/app/utils/face_clusters.py index 98eeabc4f..2dee4e0ea 100644 --- a/backend/app/utils/face_clusters.py +++ b/backend/app/utils/face_clusters.py @@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Union from numpy.typing import NDArray from app.database.connection import get_db_connection +from typing import Tuple from app.database.faces import ( db_get_all_faces_with_cluster_names, @@ -22,7 +23,6 @@ db_get_metadata, db_update_metadata, ) -from app.config.settings import DATABASE_PATH from app.logging.setup_logging import get_logger # Initialize logger @@ -122,23 +122,22 @@ def cluster_util_face_clusters_sync(force_full_reclustering: bool = False): # Perform all database operations within a single transaction with get_db_connection() as conn: - cursor = conn.cursor() # Clear old clusters first - db_delete_all_clusters(cursor) + db_delete_all_clusters(conn) # Insert the new clusters into database first - db_insert_clusters_batch(cluster_list, cursor) + db_insert_clusters_batch(cluster_list, conn) # Now update face cluster assignments (foreign keys will be valid) - db_update_face_cluster_ids_batch(results, cursor) + db_update_face_cluster_ids_batch(results, conn) # Finally, generate and update face images for each cluster for cluster_id in unique_clusters.keys(): - face_image_base64 = _generate_cluster_face_image(cluster_id, cursor) + face_image_base64 = _generate_cluster_face_image(cluster_id, conn) if face_image_base64: # Update the cluster with the generated face image success = _update_cluster_face_image( - cluster_id, face_image_base64, cursor + cluster_id, face_image_base64, conn ) if not success: raise RuntimeError( @@ -148,13 +147,12 @@ def cluster_util_face_clusters_sync(force_full_reclustering: bool = False): # Update metadata with new reclustering time, preserving other values current_metadata = metadata or {} current_metadata["reclustering_time"] = datetime.now().timestamp() - db_update_metadata(current_metadata, cursor) + db_update_metadata(current_metadata, conn) return len(cluster_list) else: face_cluster_mappings = cluster_util_assign_cluster_to_faces_without_clusterId() with get_db_connection() as conn: - cursor = conn.cursor() - db_update_face_cluster_ids_batch(face_cluster_mappings, cursor) + db_update_face_cluster_ids_batch(face_cluster_mappings, conn) return len(face_cluster_mappings) @@ -336,59 +334,63 @@ def _calculate_cosine_distances( def _update_cluster_face_image( - cluster_id: str, face_image_base64: str, cursor: Optional[sqlite3.Cursor] = None + cluster_id: str, face_image_base64: str, conn: Optional[sqlite3.Connection] = None ) -> bool: """ Update the face image for a specific cluster. Args: - cluster_id: The UUID of the cluster - face_image_base64: Base64 encoded face image string - cursor: Optional existing database cursor. If None, creates a new connection. + cluster_id: The UUID of the cluster. + face_image_base64: Base64 encoded face image string. + conn: Optional existing database connection. If None, uses context manager. Returns: - True if update was successful, False otherwise + True if update was successful, False otherwise. """ - own_connection = cursor is None - if own_connection: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - try: + def perform_update(connection: sqlite3.Connection) -> bool: + cursor = connection.cursor() cursor.execute( "UPDATE face_clusters SET face_image_base64 = ? WHERE cluster_id = ?", (face_image_base64, cluster_id), ) - success = cursor.rowcount > 0 - if own_connection: - conn.commit() - return success + return cursor.rowcount > 0 + + # Case 1: External connection provided + if conn is not None: + try: + success = perform_update(conn) + return success + except Exception as e: + logger.error(f"Error updating face image for cluster {cluster_id}: {e}") + raise + + # Case 2: Use managed connection + try: + with get_db_connection() as conn: + success = perform_update(conn) + return success except Exception as e: logger.error(f"Error updating face image for cluster {cluster_id}: {e}") - if own_connection: - conn.rollback() - return False - raise - finally: - if own_connection: - conn.close() def _get_cluster_face_data( - cluster_uuid: str, cursor: sqlite3.Cursor -) -> Optional[tuple]: + cluster_uuid: str, conn: Optional[sqlite3.Connection] = None +) -> Optional[Tuple[str, dict]]: """ Get the image path and bounding box for the first face in a cluster. Args: - cluster_uuid: The UUID of the cluster - cursor: SQLite cursor from an active transaction + cluster_uuid: The UUID of the cluster. + conn: Optional existing database connection. If None, uses context manager. Returns: - Tuple of (image_path, bbox_dict) or None if not found + Tuple of (image_path, bbox_dict) or None if not found or error occurs. """ - try: + + def perform_query(conn: sqlite3.Connection) -> Optional[Tuple[str, dict]]: + cursor = conn.cursor() cursor.execute( """ SELECT i.path, f.bbox @@ -413,8 +415,21 @@ def _get_cluster_face_data( bbox = json.loads(bbox_json) return (image_path, bbox) except json.JSONDecodeError: + logger.warning(f"Invalid JSON for bbox in cluster {cluster_uuid}") return None + # Case 1: Use existing connection + if conn is not None: + try: + return perform_query(conn) + except Exception as e: + logger.error(f"Error getting face data for cluster {cluster_uuid}: {e}") + raise + + # Case 2: Use managed connection + try: + with get_db_connection() as conn: + return perform_query(conn) except Exception as e: logger.error(f"Error getting face data for cluster {cluster_uuid}: {e}") return None @@ -538,21 +553,21 @@ def _encode_image_to_base64(img: np.ndarray, format: str = ".jpg") -> Optional[s def _generate_cluster_face_image( - cluster_uuid: str, cursor: sqlite3.Cursor + cluster_uuid: str, conn: sqlite3.Connection ) -> Optional[str]: """ Generate a base64 encoded face image for a cluster. Args: cluster_uuid: The UUID of the cluster - cursor: SQLite cursor from an active transaction + conn: Existing database connection Returns: Base64 encoded face image string, or None if generation fails """ try: # Get face data from database - face_data = _get_cluster_face_data(cluster_uuid, cursor) + face_data = _get_cluster_face_data(cluster_uuid, conn) if not face_data: return None