diff --git a/backend/app/database/albums.py b/backend/app/database/albums.py index fbad8a9bf..74a81f6dc 100644 --- a/backend/app/database/albums.py +++ b/backend/app/database/albums.py @@ -40,69 +40,83 @@ def get_db_connection(): def db_create_albums_table() -> None: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS albums ( - album_id TEXT PRIMARY KEY, - album_name TEXT UNIQUE, - description TEXT, - is_hidden BOOLEAN DEFAULT 0, - password_hash TEXT + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS albums ( + album_id TEXT PRIMARY KEY, + album_name TEXT UNIQUE, + description TEXT, + is_hidden BOOLEAN DEFAULT 0, + password_hash TEXT + ) + """ ) - """ - ) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() def db_create_album_images_table() -> None: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS album_images ( - album_id TEXT, - image_id TEXT, - PRIMARY KEY (album_id, image_id), - FOREIGN KEY (album_id) REFERENCES albums(album_id) ON DELETE CASCADE, - FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS album_images ( + album_id TEXT, + image_id TEXT, + PRIMARY KEY (album_id, image_id), + FOREIGN KEY (album_id) REFERENCES albums(album_id) ON DELETE CASCADE, + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE + ) + """ ) - """ - ) - conn.commit() - conn.close() + 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() - if show_hidden: - cursor.execute("SELECT * FROM albums") - else: - cursor.execute("SELECT * FROM albums WHERE is_hidden = 0") - albums = cursor.fetchall() - conn.close() - return albums + try: + 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() - cursor.execute("SELECT * FROM albums WHERE album_name = ?", (name,)) - album = cursor.fetchone() - conn.close() - return album if album else None + try: + 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() - cursor.execute("SELECT * FROM albums WHERE album_id = ?", (album_id,)) - album = cursor.fetchone() - conn.close() - return album if album else None + try: + 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( @@ -114,20 +128,22 @@ def db_insert_album( ): conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - password_hash = None - if password: - password_hash = bcrypt.hashpw( - password.encode("utf-8"), bcrypt.gensalt() - ).decode("utf-8") - cursor.execute( - """ - INSERT INTO albums (album_id, album_name, description, is_hidden, password_hash) - VALUES (?, ?, ?, ?, ?) - """, - (album_id, album_name, description, int(is_hidden), password_hash), - ) - conn.commit() - conn.close() + try: + password_hash = None + if password: + password_hash = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + cursor.execute( + """ + INSERT INTO albums (album_id, album_name, description, is_hidden, password_hash) + VALUES (?, ?, ?, ?, ?) + """, + (album_id, album_name, description, int(is_hidden), password_hash), + ) + conn.commit() + finally: + conn.close() def db_update_album( @@ -139,38 +155,52 @@ def db_update_album( ): conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - password_hash = None - if password: - password_hash = bcrypt.hashpw( - password.encode("utf-8"), bcrypt.gensalt() - ).decode("utf-8") - cursor.execute( - """ - UPDATE albums - SET album_name = ?, description = ?, is_hidden = ?, password_hash = ? - WHERE album_id = ? - """, - (album_name, description, int(is_hidden), password_hash, album_id), - ) - conn.commit() - conn.close() + try: + if password is not None: + # Update with new password + password_hash = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + cursor.execute( + """ + UPDATE albums + SET album_name = ?, description = ?, is_hidden = ?, password_hash = ? + WHERE album_id = ? + """, + (album_name, description, int(is_hidden), password_hash, album_id), + ) + else: + # Update without changing password + cursor.execute( + """ + UPDATE albums + SET album_name = ?, description = ?, is_hidden = ? + WHERE album_id = ? + """, + (album_name, description, int(is_hidden), album_id), + ) + conn.commit() + finally: + conn.close() def db_delete_album(album_id: str): - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute("DELETE FROM albums WHERE album_id = ?", (album_id,)) - conn.commit() - conn.close() + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM albums WHERE album_id = ?", (album_id,)) def db_get_album_images(album_id: str): conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("SELECT image_id FROM album_images WHERE album_id = ?", (album_id,)) - images = cursor.fetchall() - conn.close() - return [img[0] for img in images] + try: + 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]): @@ -214,20 +244,26 @@ 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() - cursor.executemany( - "DELETE FROM album_images WHERE album_id = ? AND image_id = ?", - [(album_id, img_id) for img_id in image_ids], - ) - conn.commit() - conn.close() + try: + 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() - cursor.execute("SELECT password_hash FROM albums WHERE album_id = ?", (album_id,)) - row = cursor.fetchone() - conn.close() - if not row or not row[0]: - return False - return bcrypt.checkpw(password.encode("utf-8"), row[0].encode("utf-8")) + try: + cursor.execute( + "SELECT password_hash FROM albums WHERE album_id = ?", (album_id,) + ) + row = cursor.fetchone() + 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/face_clusters.py b/backend/app/database/face_clusters.py index 5bfa956e2..dc8c97334 100644 --- a/backend/app/database/face_clusters.py +++ b/backend/app/database/face_clusters.py @@ -20,19 +20,23 @@ class ClusterData(TypedDict): def db_create_clusters_table() -> None: """Create the face_clusters table if it doesn't exist.""" - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS face_clusters ( + cluster_id TEXT PRIMARY KEY, + cluster_name TEXT, + face_image_base64 TEXT + ) """ - CREATE TABLE IF NOT EXISTS face_clusters ( - cluster_id TEXT PRIMARY KEY, - cluster_name TEXT, - face_image_base64 TEXT ) - """ - ) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() def db_insert_clusters_batch(clusters: List[ClusterData]) -> List[ClusterId]: @@ -52,29 +56,30 @@ def db_insert_clusters_batch(clusters: List[ClusterData]) -> List[ClusterId]: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cluster_ids = [] - insert_data = [] + try: + cluster_ids = [] + insert_data = [] - for cluster in clusters: - cluster_id = cluster.get("cluster_id") - cluster_name = cluster.get("cluster_name") - face_image_base64 = cluster.get("face_image_base64") + for cluster in clusters: + cluster_id = cluster.get("cluster_id") + cluster_name = cluster.get("cluster_name") + face_image_base64 = cluster.get("face_image_base64") - insert_data.append((cluster_id, cluster_name, face_image_base64)) - cluster_ids.append(cluster_id) + insert_data.append((cluster_id, cluster_name, face_image_base64)) + cluster_ids.append(cluster_id) - cursor.executemany( - """ - INSERT INTO face_clusters (cluster_id, cluster_name, face_image_base64) - VALUES (?, ?, ?) - """, - insert_data, - ) - - conn.commit() - conn.close() + cursor.executemany( + """ + INSERT INTO face_clusters (cluster_id, cluster_name, face_image_base64) + VALUES (?, ?, ?) + """, + insert_data, + ) - return cluster_ids + conn.commit() + return cluster_ids + finally: + conn.close() def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]: @@ -90,19 +95,21 @@ def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters WHERE cluster_id = ?", - (cluster_id,), - ) + try: + cursor.execute( + "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters WHERE cluster_id = ?", + (cluster_id,), + ) - row = cursor.fetchone() - conn.close() + row = cursor.fetchone() - if row: - return ClusterData( - cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2] - ) - return None + if row: + return 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]: @@ -115,22 +122,24 @@ def db_get_all_clusters() -> List[ClusterData]: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters ORDER BY cluster_id" - ) + try: + cursor.execute( + "SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters ORDER BY cluster_id" + ) - rows = cursor.fetchall() - conn.close() + rows = cursor.fetchall() - clusters = [] - for row in rows: - clusters.append( - ClusterData( - cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2] + clusters = [] + for row in rows: + clusters.append( + ClusterData( + cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2] + ) ) - ) - return clusters + return clusters + finally: + conn.close() def db_update_cluster( @@ -150,30 +159,30 @@ def db_update_cluster( conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - # Build the update query dynamically based on provided parameters - update_fields = [] - update_values = [] - - if cluster_name is not None: - update_fields.append("cluster_name = ?") - update_values.append(cluster_name) + try: + # Build the update query dynamically based on provided parameters + update_fields = [] + update_values = [] - if not update_fields: - conn.close() - return False + if cluster_name is not None: + update_fields.append("cluster_name = ?") + update_values.append(cluster_name) - update_values.append(cluster_id) + if not update_fields: + return False - cursor.execute( - f"UPDATE face_clusters SET {', '.join(update_fields)} WHERE cluster_id = ?", - update_values, - ) + update_values.append(cluster_id) - updated = cursor.rowcount > 0 - conn.commit() - conn.close() + cursor.execute( + f"UPDATE face_clusters SET {', '.join(update_fields)} WHERE cluster_id = ?", + update_values, + ) - return updated + updated = cursor.rowcount > 0 + conn.commit() + return updated + finally: + conn.close() def db_delete_all_clusters() -> int: @@ -186,18 +195,19 @@ def db_delete_all_clusters() -> int: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("DELETE FROM face_clusters") + try: + cursor.execute("DELETE FROM face_clusters") - deleted_count = cursor.rowcount - conn.commit() - conn.close() - - return deleted_count + deleted_count = cursor.rowcount + conn.commit() + return deleted_count + finally: + conn.close() -def db_get_all_clusters_with_face_counts() -> List[ - Dict[str, Union[str, Optional[str], int]] -]: +def db_get_all_clusters_with_face_counts() -> ( + List[Dict[str, Union[str, Optional[str], int]]] +): """ Retrieve all clusters with their face counts and stored face images. @@ -207,36 +217,38 @@ def db_get_all_clusters_with_face_counts() -> List[ conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - """ - SELECT - fc.cluster_id, - fc.cluster_name, - COUNT(f.face_id) as face_count, - fc.face_image_base64 - FROM face_clusters fc - LEFT JOIN faces f ON fc.cluster_id = f.cluster_id - GROUP BY fc.cluster_id, fc.cluster_name, fc.face_image_base64 - ORDER BY fc.cluster_id - """ - ) - - rows = cursor.fetchall() - conn.close() - - clusters = [] - for row in rows: - cluster_id, cluster_name, face_count, face_image_base64 = row - clusters.append( - { - "cluster_id": cluster_id, - "cluster_name": cluster_name, - "face_count": face_count, - "face_image_base64": face_image_base64, - } + try: + cursor.execute( + """ + SELECT + fc.cluster_id, + fc.cluster_name, + COUNT(f.face_id) as face_count, + fc.face_image_base64 + FROM face_clusters fc + LEFT JOIN faces f ON fc.cluster_id = f.cluster_id + GROUP BY fc.cluster_id, fc.cluster_name, fc.face_image_base64 + ORDER BY fc.cluster_id + """ ) - return clusters + rows = cursor.fetchall() + + clusters = [] + for row in rows: + cluster_id, cluster_name, face_count, face_image_base64 = row + clusters.append( + { + "cluster_id": cluster_id, + "cluster_name": cluster_name, + "face_count": face_count, + "face_image_base64": face_image_base64, + } + ) + + return clusters + finally: + conn.close() def db_get_images_by_cluster_id( @@ -254,58 +266,58 @@ def db_get_images_by_cluster_id( conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - """ - SELECT DISTINCT - i.id as image_id, - i.path as image_path, - i.thumbnailPath as thumbnail_path, - i.metadata, - f.face_id, - f.confidence, - f.bbox - FROM images i - INNER JOIN faces f ON i.id = f.image_id - WHERE f.cluster_id = ? - ORDER BY i.path - """, - (cluster_id,), - ) - - rows = cursor.fetchall() - conn.close() - - from app.utils.images import image_util_parse_metadata - - images = [] - for row in rows: - ( - image_id, - image_path, - thumbnail_path, - metadata, - face_id, - confidence, - bbox_json, - ) = row - - # Parse bbox JSON if it exists - bbox = None - if bbox_json: - import json - - bbox = json.loads(bbox_json) - - images.append( - { - "image_id": image_id, - "image_path": image_path, - "thumbnail_path": thumbnail_path, - "metadata": image_util_parse_metadata(metadata), - "face_id": face_id, - "confidence": confidence, - "bbox": bbox, - } + try: + cursor.execute( + """ + SELECT DISTINCT + i.id as image_id, + i.path as image_path, + i.thumbnailPath as thumbnail_path, + i.metadata, + f.face_id, + f.confidence, + f.bbox + FROM images i + INNER JOIN faces f ON i.id = f.image_id + WHERE f.cluster_id = ? + ORDER BY i.path + """, + (cluster_id,), ) - return images + rows = cursor.fetchall() + + images = [] + for row in rows: + ( + image_id, + image_path, + thumbnail_path, + metadata, + face_id, + confidence, + bbox_json, + ) = row + + # Parse bbox JSON if it exists + bbox = None + if bbox_json: + import json + + bbox = json.loads(bbox_json) + + images.append( + { + "image_id": image_id, + "image_path": image_path, + "thumbnail_path": thumbnail_path, + "metadata": metadata, + "face_id": face_id, + "confidence": confidence, + "bbox": bbox, + } + ) + + return images + finally: + conn.close() diff --git a/backend/app/database/faces.py b/backend/app/database/faces.py index 9a8a0269a..6b0e6bd9a 100644 --- a/backend/app/database/faces.py +++ b/backend/app/database/faces.py @@ -27,24 +27,29 @@ class FaceData(TypedDict): def db_create_faces_table() -> None: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + conn.execute("PRAGMA foreign_keys = ON") + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS faces ( + face_id INTEGER PRIMARY KEY AUTOINCREMENT, + image_id TEXT, + cluster_id INTEGER, + embeddings TEXT, + confidence REAL, + bbox TEXT, + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, + FOREIGN KEY (cluster_id) REFERENCES face_clusters(cluster_id) ON DELETE SET NULL + ) """ - CREATE TABLE IF NOT EXISTS faces ( - face_id INTEGER PRIMARY KEY AUTOINCREMENT, - image_id INTEGER, - cluster_id INTEGER, - embeddings TEXT, - confidence REAL, - bbox TEXT, - FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, - FOREIGN KEY (cluster_id) REFERENCES face_clusters(cluster_id) ON DELETE SET NULL ) - """ - ) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() def db_insert_face_embeddings( @@ -68,23 +73,25 @@ def db_insert_face_embeddings( conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - embeddings_json = json.dumps([emb.tolist() for emb in embeddings]) + try: + embeddings_json = json.dumps([emb.tolist() for emb in embeddings]) - # Convert bbox to JSON string if provided - bbox_json = json.dumps(bbox) if bbox is not None else None + # Convert bbox to JSON string if provided + bbox_json = json.dumps(bbox) if bbox is not None else None - cursor.execute( - """ - INSERT INTO faces (image_id, cluster_id, embeddings, confidence, bbox) - VALUES (?, ?, ?, ?, ?) - """, - (image_id, cluster_id, embeddings_json, confidence, bbox_json), - ) + cursor.execute( + """ + INSERT INTO faces (image_id, cluster_id, embeddings, confidence, bbox) + VALUES (?, ?, ?, ?, ?) + """, + (image_id, cluster_id, embeddings_json, confidence, bbox_json), + ) - face_id = cursor.lastrowid - conn.commit() - conn.close() - return face_id + face_id = cursor.lastrowid + conn.commit() + return face_id + finally: + conn.close() def db_insert_face_embeddings_by_image_id( @@ -219,24 +226,26 @@ def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbed conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("SELECT face_id, embeddings FROM faces WHERE cluster_id IS NULL") + try: + cursor.execute("SELECT face_id, embeddings FROM faces WHERE cluster_id IS NULL") - rows = cursor.fetchall() - conn.close() + rows = cursor.fetchall() - faces = [] - for row in rows: - face_id, embeddings_json = row - # Convert JSON string back to numpy array - embeddings = np.array(json.loads(embeddings_json)) - faces.append({"face_id": face_id, "embeddings": embeddings}) + faces = [] + for row in rows: + face_id, embeddings_json = row + # Convert JSON string back to numpy array + embeddings = np.array(json.loads(embeddings_json)) + faces.append({"face_id": face_id, "embeddings": embeddings}) - return faces + return faces + finally: + conn.close() -def db_get_all_faces_with_cluster_names() -> List[ - Dict[str, Union[FaceId, FaceEmbedding, Optional[str]]] -]: +def db_get_all_faces_with_cluster_names() -> ( + List[Dict[str, Union[FaceId, FaceEmbedding, Optional[str]]]] +): """ Get all faces with their corresponding cluster names. @@ -246,32 +255,38 @@ def db_get_all_faces_with_cluster_names() -> List[ conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - """ - SELECT f.face_id, f.embeddings, fc.cluster_name - FROM faces f - LEFT JOIN face_clusters fc ON f.cluster_id = fc.cluster_id - ORDER BY f.face_id - """ - ) - - rows = cursor.fetchall() - conn.close() - - faces = [] - for row in rows: - face_id, embeddings_json, cluster_name = row - # Convert JSON string back to numpy array - embeddings = np.array(json.loads(embeddings_json)) - faces.append( - {"face_id": face_id, "embeddings": embeddings, "cluster_name": cluster_name} + try: + cursor.execute( + """ + SELECT f.face_id, f.embeddings, fc.cluster_name + FROM faces f + LEFT JOIN face_clusters fc ON f.cluster_id = fc.cluster_id + ORDER BY f.face_id + """ ) - return faces + rows = cursor.fetchall() + + faces = [] + for row in rows: + face_id, embeddings_json, cluster_name = row + # Convert JSON string back to numpy array + embeddings = np.array(json.loads(embeddings_json)) + faces.append( + { + "face_id": face_id, + "embeddings": embeddings, + "cluster_name": cluster_name, + } + ) + + return faces + finally: + conn.close() def db_update_face_cluster_ids_batch( - face_cluster_mapping: List[Dict[str, Union[FaceId, ClusterId]]] + face_cluster_mapping: List[Dict[str, Union[FaceId, ClusterId]]], ) -> None: """ Update cluster IDs for multiple faces in batch. @@ -293,24 +308,26 @@ def db_update_face_cluster_ids_batch( conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - # Prepare update data as tuples (cluster_id, face_id) - update_data = [] - for mapping in face_cluster_mapping: - face_id = mapping.get("face_id") - cluster_id = mapping.get("cluster_id") - update_data.append((cluster_id, face_id)) - - cursor.executemany( - """ - UPDATE faces - SET cluster_id = ? - WHERE face_id = ? - """, - update_data, - ) + try: + # Prepare update data as tuples (cluster_id, face_id) + update_data = [] + for mapping in face_cluster_mapping: + face_id = mapping.get("face_id") + cluster_id = mapping.get("cluster_id") + update_data.append((cluster_id, face_id)) + + cursor.executemany( + """ + UPDATE faces + SET cluster_id = ? + WHERE face_id = ? + """, + update_data, + ) - conn.commit() - conn.close() + conn.commit() + finally: + conn.close() def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding]]]: @@ -324,41 +341,43 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding] conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute( - """ - SELECT f.cluster_id, f.embeddings - FROM faces f - WHERE f.cluster_id IS NOT NULL - ORDER BY f.cluster_id - """ - ) - - rows = cursor.fetchall() - conn.close() - - if not rows: - return [] - - # Group embeddings by cluster_id - cluster_embeddings = {} - for row in rows: - cluster_id, embeddings_json = row - # Convert JSON string back to numpy array - embeddings = np.array(json.loads(embeddings_json)) - - if cluster_id not in cluster_embeddings: - cluster_embeddings[cluster_id] = [] - cluster_embeddings[cluster_id].append(embeddings) - - # Calculate mean embeddings for each cluster - cluster_means = [] - for cluster_id, embeddings_list in cluster_embeddings.items(): - # Stack all embeddings for this cluster and calculate mean - stacked_embeddings = np.stack(embeddings_list) - mean_embedding = np.mean(stacked_embeddings, axis=0) - - cluster_means.append( - {"cluster_id": cluster_id, "mean_embedding": mean_embedding} + try: + cursor.execute( + """ + SELECT f.cluster_id, f.embeddings + FROM faces f + WHERE f.cluster_id IS NOT NULL + ORDER BY f.cluster_id + """ ) - return cluster_means + rows = cursor.fetchall() + + if not rows: + return [] + + # Group embeddings by cluster_id + cluster_embeddings = {} + for row in rows: + cluster_id, embeddings_json = row + # Convert JSON string back to numpy array + embeddings = np.array(json.loads(embeddings_json)) + + if cluster_id not in cluster_embeddings: + cluster_embeddings[cluster_id] = [] + cluster_embeddings[cluster_id].append(embeddings) + + # Calculate mean embeddings for each cluster + cluster_means = [] + for cluster_id, embeddings_list in cluster_embeddings.items(): + # Stack all embeddings for this cluster and calculate mean + stacked_embeddings = np.stack(embeddings_list) + mean_embedding = np.mean(stacked_embeddings, axis=0) + + cluster_means.append( + {"cluster_id": cluster_id, "mean_embedding": mean_embedding} + ) + + return cluster_means + finally: + conn.close() diff --git a/backend/app/database/folders.py b/backend/app/database/folders.py index 383628d1c..3a2ac976d 100644 --- a/backend/app/database/folders.py +++ b/backend/app/database/folders.py @@ -13,23 +13,27 @@ def db_create_folders_table() -> None: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS folders ( - folder_id TEXT PRIMARY KEY, - parent_folder_id TEXT, - folder_path TEXT UNIQUE, - last_modified_time INTEGER, - AI_Tagging BOOLEAN, - taggingCompleted BOOLEAN, - FOREIGN KEY (parent_folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS folders ( + folder_id TEXT PRIMARY KEY, + parent_folder_id TEXT, + folder_path TEXT UNIQUE, + last_modified_time INTEGER, + AI_Tagging BOOLEAN, + taggingCompleted BOOLEAN, + FOREIGN KEY (parent_folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE + ) + """ ) - """ - ) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() def db_insert_folders_batch(folders_data: List[FolderData]) -> None: @@ -64,67 +68,71 @@ def db_insert_folder( conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - 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.") + 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.") - cursor.execute( - "SELECT folder_id FROM folders WHERE folder_path = ?", - (abs_folder_path,), - ) - existing_folder = cursor.fetchone() + cursor.execute( + "SELECT folder_id FROM folders WHERE folder_path = ?", + (abs_folder_path,), + ) + existing_folder = cursor.fetchone() - if existing_folder: - result = existing_folder[0] - conn.close() - return result + if existing_folder: + return existing_folder[0] - # Time is in Unix format - last_modified_time = int(os.path.getmtime(abs_folder_path)) + # Time is in Unix format + last_modified_time = int(os.path.getmtime(abs_folder_path)) - if folder_id is None: - folder_id = str(uuid.uuid4()) + if folder_id is None: + folder_id = str(uuid.uuid4()) - cursor.execute( - "INSERT INTO folders (folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, taggingCompleted) VALUES (?, ?, ?, ?, ?, ?)", - ( - folder_id, - abs_folder_path, - parent_folder_id, - last_modified_time, - AI_Tagging, - taggingCompleted, - ), - ) + cursor.execute( + "INSERT INTO folders (folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, taggingCompleted) VALUES (?, ?, ?, ?, ?, ?)", + ( + folder_id, + abs_folder_path, + parent_folder_id, + last_modified_time, + AI_Tagging, + taggingCompleted, + ), + ) - conn.commit() - conn.close() - return folder_id + 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() - abs_folder_path = os.path.abspath(folder_path) - cursor.execute( - "SELECT folder_id FROM folders WHERE folder_path = ?", - (abs_folder_path,), - ) - result = cursor.fetchone() - conn.close() - return result[0] if result else None + try: + abs_folder_path = os.path.abspath(folder_path) + cursor.execute( + "SELECT folder_id FROM folders WHERE folder_path = ?", + (abs_folder_path,), + ) + 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() - cursor.execute( - "SELECT folder_path FROM folders WHERE folder_id = ?", - (folder_id,), - ) - result = cursor.fetchone() - conn.close() - return result[0] if result else None + try: + 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]: @@ -136,9 +144,12 @@ def db_get_all_folders() -> List[FolderPath]: def db_get_all_folder_ids() -> List[FolderId]: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("SELECT folder_id from folders") - rows = cursor.fetchall() - return [row[0] for row in rows] if rows else [] + try: + 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: @@ -179,30 +190,31 @@ def db_delete_folders_batch(folder_ids: List[FolderId]) -> int: def db_delete_folder(folder_path: FolderPath) -> None: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - abs_folder_path = os.path.abspath(folder_path) - cursor.execute( - "PRAGMA foreign_keys = ON;" - ) # Important for deleting rows in image_id_mapping and images table because they reference this folder_id - conn.commit() - cursor.execute( - "SELECT folder_id FROM folders WHERE folder_path = ?", - (abs_folder_path,), - ) - existing_folder = cursor.fetchone() - - if not existing_folder: - conn.close() - raise ValueError( - f"Error: Folder '{folder_path}' does not exist in the database." + try: + abs_folder_path = os.path.abspath(folder_path) + cursor.execute( + "PRAGMA foreign_keys = ON;" + ) # Important for deleting rows in image_id_mapping and images table because they reference this folder_id + conn.commit() + cursor.execute( + "SELECT folder_id FROM folders WHERE folder_path = ?", + (abs_folder_path,), ) + existing_folder = cursor.fetchone() - cursor.execute( - "DELETE FROM folders WHERE folder_path = ?", - (abs_folder_path,), - ) + if not existing_folder: + raise ValueError( + f"Error: Folder '{folder_path}' does not exist in the database." + ) - conn.commit() - conn.close() + cursor.execute( + "DELETE FROM folders WHERE folder_path = ?", + (abs_folder_path,), + ) + + conn.commit() + finally: + conn.close() def db_update_parent_ids_for_subtree( @@ -382,9 +394,9 @@ def db_get_folder_ids_by_paths( conn.close() -def db_get_all_folder_details() -> List[ - Tuple[str, str, Optional[str], int, bool, Optional[bool]] -]: +def db_get_all_folder_details() -> ( + List[Tuple[str, str, Optional[str], int, bool, Optional[bool]]] +): """ Get all folder details including folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, and taggingCompleted. diff --git a/backend/app/database/metadata.py b/backend/app/database/metadata.py index fbd13fe88..26ededb0a 100644 --- a/backend/app/database/metadata.py +++ b/backend/app/database/metadata.py @@ -7,23 +7,27 @@ def db_create_metadata_table() -> None: """Create the metadata table if it doesn't exist.""" - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS metadata ( + metadata TEXT + ) """ - CREATE TABLE IF NOT EXISTS metadata ( - metadata TEXT ) - """ - ) - # Insert initial row if table is empty - cursor.execute("SELECT COUNT(*) FROM metadata") - if cursor.fetchone()[0] == 0: - cursor.execute("INSERT INTO metadata (metadata) VALUES (?)", ("{}",)) + # Insert initial row if table is empty + cursor.execute("SELECT COUNT(*) FROM metadata") + if cursor.fetchone()[0] == 0: + cursor.execute("INSERT INTO metadata (metadata) VALUES (?)", ("{}",)) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() def db_get_metadata() -> Optional[Dict[str, Any]]: @@ -36,17 +40,19 @@ def db_get_metadata() -> Optional[Dict[str, Any]]: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("SELECT metadata FROM metadata LIMIT 1") + try: + cursor.execute("SELECT metadata FROM metadata LIMIT 1") - row = cursor.fetchone() - conn.close() + row = cursor.fetchone() - if row and row[0]: - try: - return json.loads(row[0]) - except json.JSONDecodeError: - return None - return None + if row and row[0]: + try: + return json.loads(row[0]) + except json.JSONDecodeError: + return None + return None + finally: + conn.close() def db_update_metadata(metadata: Dict[str, Any]) -> bool: @@ -62,14 +68,15 @@ def db_update_metadata(metadata: Dict[str, Any]) -> bool: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - 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,)) + try: + metadata_json = json.dumps(metadata) - updated = cursor.rowcount > 0 - conn.commit() - conn.close() + # Delete all existing rows and insert new one + cursor.execute("DELETE FROM metadata") + cursor.execute("INSERT INTO metadata (metadata) VALUES (?)", (metadata_json,)) - return updated + updated = cursor.rowcount > 0 + conn.commit() + return updated + finally: + conn.close() diff --git a/backend/app/database/yolo_mapping.py b/backend/app/database/yolo_mapping.py index 4711aa085..af5c18927 100644 --- a/backend/app/database/yolo_mapping.py +++ b/backend/app/database/yolo_mapping.py @@ -8,22 +8,28 @@ def db_create_YOLO_classes_table(): import os print(os.getcwd()) - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS mappings ( - class_id TEXT PRIMARY KEY, - name VARCHAR NOT NULL - ) - """ - ) - for class_id, name in enumerate(class_names): + conn = None + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() cursor.execute( - "INSERT OR REPLACE INTO mappings (class_id, name) VALUES (?, ?)", - (str(class_id), name), # Convert class_id to string since it's now TEXT + """ + CREATE TABLE IF NOT EXISTS mappings ( + class_id INTEGER PRIMARY KEY, + name VARCHAR NOT NULL + ) + """ ) + for class_id, name in enumerate(class_names): + cursor.execute( + "INSERT OR REPLACE INTO mappings (class_id, name) VALUES (?, ?)", + ( + class_id, + name, + ), # Keep class_id as integer to match image_classes.class_id + ) - conn.commit() - conn.close() + conn.commit() + finally: + if conn is not None: + conn.close() diff --git a/backend/app/routes/albums.py b/backend/app/routes/albums.py index 5126b81c5..ae0408613 100644 --- a/backend/app/routes/albums.py +++ b/backend/app/routes/albums.py @@ -29,6 +29,7 @@ router = APIRouter() + # GET /albums/ - Get all albums @router.get("/", response_model=GetAlbumsResponse) def get_albums(show_hidden: bool = Query(False)): diff --git a/backend/app/utils/images.py b/backend/app/utils/images.py index fab19ef5b..d31b38184 100644 --- a/backend/app/utils/images.py +++ b/backend/app/utils/images.py @@ -99,7 +99,7 @@ def image_util_process_untagged_images() -> bool: def image_util_classify_and_face_detect_images( - untagged_images: List[Dict[str, str]] + untagged_images: List[Dict[str, str]], ) -> None: """Classify untagged images and detect faces if applicable.""" object_classifier = ObjectClassifier() @@ -262,7 +262,7 @@ def image_util_remove_obsolete_images(folder_id_list: List[int]) -> int: def image_util_create_folder_path_mapping( - folder_ids: List[Tuple[int, str]] + folder_ids: List[Tuple[int, str]], ) -> Dict[str, int]: """ Create a dictionary mapping folder paths to their IDs. diff --git a/sync-microservice/app/database/folders.py b/sync-microservice/app/database/folders.py index f6633b6e9..8f413650e 100644 --- a/sync-microservice/app/database/folders.py +++ b/sync-microservice/app/database/folders.py @@ -48,10 +48,10 @@ def db_check_database_connection() -> bool: Returns: True if connection is successful and table exists, False otherwise """ + conn = None try: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - # Check if folders table exists cursor.execute( """ @@ -60,12 +60,13 @@ def db_check_database_connection() -> bool: """ ) result = cursor.fetchone() - conn.close() - return result is not None except Exception as e: print(f"Database connection error: {e}") return False + finally: + if conn is not None: + conn.close() def db_get_tagging_progress() -> List[FolderTaggingInfo]: