Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions backend/app/database/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ def db_create_albums_table() -> None:
album_id TEXT PRIMARY KEY,
album_name TEXT UNIQUE,
description TEXT,
is_hidden BOOLEAN DEFAULT 0,
password_hash TEXT
is_locked BOOLEAN DEFAULT 0,
password_hash TEXT,
cover_image_path TEXT
)
"""
)
Expand Down Expand Up @@ -48,14 +49,14 @@ def db_create_album_images_table() -> None:
conn.close()


def db_get_all_albums(show_hidden: bool = False):
def db_get_all_albums():
"""Get all albums (both locked and unlocked)."""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
if show_hidden:
cursor.execute("SELECT * FROM albums")
else:
cursor.execute("SELECT * FROM albums WHERE is_hidden = 0")
cursor.execute(
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums"
)
albums = cursor.fetchall()
return albums
finally:
Expand All @@ -66,7 +67,10 @@ def db_get_album_by_name(name: str):
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM albums WHERE album_name = ?", (name,))
cursor.execute(
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums WHERE album_name = ?",
(name,),
)
album = cursor.fetchone()
return album if album else None
finally:
Expand All @@ -77,7 +81,10 @@ def db_get_album(album_id: str):
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM albums WHERE album_id = ?", (album_id,))
cursor.execute(
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums WHERE album_id = ?",
(album_id,),
)
album = cursor.fetchone()
return album if album else None
finally:
Expand All @@ -88,7 +95,7 @@ def db_insert_album(
album_id: str,
album_name: str,
description: str = "",
is_hidden: bool = False,
is_locked: bool = False,
password: str = None,
):
conn = sqlite3.connect(DATABASE_PATH)
Expand All @@ -101,10 +108,10 @@ def db_insert_album(
).decode("utf-8")
cursor.execute(
"""
INSERT INTO albums (album_id, album_name, description, is_hidden, password_hash)
INSERT INTO albums (album_id, album_name, description, is_locked, password_hash)
VALUES (?, ?, ?, ?, ?)
""",
(album_id, album_name, description, int(is_hidden), password_hash),
(album_id, album_name, description, int(is_locked), password_hash),
)
conn.commit()
finally:
Expand All @@ -115,7 +122,7 @@ def db_update_album(
album_id: str,
album_name: str,
description: str,
is_hidden: bool,
is_locked: bool,
password: str = None,
):
conn = sqlite3.connect(DATABASE_PATH)
Expand All @@ -129,20 +136,20 @@ def db_update_album(
cursor.execute(
"""
UPDATE albums
SET album_name = ?, description = ?, is_hidden = ?, password_hash = ?
SET album_name = ?, description = ?, is_locked = ?, password_hash = ?
WHERE album_id = ?
""",
(album_name, description, int(is_hidden), password_hash, album_id),
(album_name, description, int(is_locked), password_hash, album_id),
)
else:
# Update without changing password
cursor.execute(
"""
UPDATE albums
SET album_name = ?, description = ?, is_hidden = ?
SET album_name = ?, description = ?, is_locked = ?
WHERE album_id = ?
""",
(album_name, description, int(is_hidden), album_id),
(album_name, description, int(is_locked), album_id),
)
conn.commit()
finally:
Expand All @@ -155,6 +162,20 @@ def db_delete_album(album_id: str):
cursor.execute("DELETE FROM albums WHERE album_id = ?", (album_id,))


def db_update_album_cover_image(album_id: str, cover_image_path: str):
"""Update the cover image path for an album"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"UPDATE albums SET cover_image_path = ? WHERE album_id = ?",
(cover_image_path, album_id),
)
conn.commit()
finally:
conn.close()


def db_get_album_images(album_id: str):
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
Expand Down Expand Up @@ -252,3 +273,15 @@ def verify_album_password(album_id: str, password: str) -> bool:
return bcrypt.checkpw(password.encode("utf-8"), row[0].encode("utf-8"))
finally:
conn.close()


def db_get_image_path(image_id: str) -> str | None:
"""Get the path of an image by its ID."""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute("SELECT path FROM images WHERE id = ?", (image_id,))
result = cursor.fetchone()
return result[0] if result else None
finally:
conn.close()
9 changes: 7 additions & 2 deletions backend/app/database/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,18 +457,22 @@ def db_toggle_image_favourite_status(image_id: str) -> bool:
finally:
conn.close()


def db_get_image_by_id(image_id: str) -> Optional[dict]:
"""
Get a single image by ID with its favorite status.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute("""
cursor.execute(
"""
SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite
FROM images
WHERE id = ?
""", (image_id,))
""",
(image_id,),
)
row = cursor.fetchone()
if not row:
return None
Expand All @@ -488,6 +492,7 @@ def db_get_image_by_id(image_id: str) -> Optional[dict]:
finally:
conn.close()


# ============================================================================
# MEMORIES FEATURE - Location and Time-based Queries
# ============================================================================
Expand Down
99 changes: 86 additions & 13 deletions backend/app/routes/albums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, status, Query, Body, Path
from fastapi import APIRouter, HTTPException, status, Body, Path
import uuid
from app.schemas.album import (
GetAlbumsResponse,
Expand All @@ -11,6 +11,7 @@
SuccessResponse,
ErrorResponse,
ImageIdsRequest,
SetCoverImageRequest,
Album,
)
from app.database.albums import (
Expand All @@ -24,24 +25,33 @@
db_add_images_to_album,
db_remove_image_from_album,
db_remove_images_from_album,
db_update_album_cover_image,
verify_album_password,
db_get_image_path,
)

router = APIRouter()


# GET /albums/ - Get all albums
# GET /albums/ - Get all albums (including locked ones)
@router.get("/", response_model=GetAlbumsResponse)
def get_albums(show_hidden: bool = Query(False)):
albums = db_get_all_albums(show_hidden)
def get_albums():
"""Get all albums. Always returns both locked and unlocked albums."""
albums = db_get_all_albums()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
album_list = []
for album in albums:
# Get image count for each album
image_ids = db_get_album_images(album[0])
image_count = len(image_ids)

album_list.append(
Album(
album_id=album[0],
album_name=album[1],
description=album[2] or "",
is_hidden=bool(album[3]),
is_locked=bool(album[3]),
cover_image_path=album[5] if len(album) > 5 else None,
image_count=image_count,
)
)
Comment on lines 42 to 56

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_albums endpoint calls db_get_album_images() for every album in a loop (line 44), which results in N+1 database queries. For users with many albums, this could cause significant performance degradation. Consider optimizing this by either: (1) adding a database query that counts images per album in a single query using a JOIN or GROUP BY, or (2) caching the image counts in the albums table itself and updating it when images are added/removed.

Copilot uses AI. Check for mistakes.
return GetAlbumsResponse(success=True, albums=album_list)
Expand All @@ -64,7 +74,7 @@ def create_album(body: CreateAlbumRequest):
album_id = str(uuid.uuid4())
try:
db_insert_album(
album_id, body.name, body.description, body.is_hidden, body.password
album_id, body.name, body.description, body.is_locked, body.password
)
return CreateAlbumResponse(success=True, album_id=album_id)
except Exception as e:
Expand All @@ -91,11 +101,17 @@ def get_album(album_id: str = Path(...)):
)

try:
# Get image count for the album
image_ids = db_get_album_images(album_id)
image_count = len(image_ids)

album_obj = Album(
album_id=album[0],
album_name=album[1],
description=album[2] or "",
is_hidden=bool(album[3]),
is_locked=bool(album[3]),
cover_image_path=album[5] if len(album) > 5 else None,
image_count=image_count,
)
return GetAlbumResponse(success=True, data=album_obj)
except Exception as e:
Expand Down Expand Up @@ -127,11 +143,11 @@ def update_album(album_id: str = Path(...), body: UpdateAlbumRequest = Body(...)
"album_id": album[0],
"album_name": album[1],
"description": album[2],
"is_hidden": bool(album[3]),
"is_locked": bool(album[3]),
"password_hash": album[4],
}

if album_dict["password_hash"]:
if album_dict["is_locked"]:
if not body.current_password:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -154,7 +170,7 @@ def update_album(album_id: str = Path(...), body: UpdateAlbumRequest = Body(...)

try:
db_update_album(
album_id, body.name, body.description, body.is_hidden, body.password
album_id, body.name, body.description, body.is_locked, body.password
)
return SuccessResponse(success=True, msg="Album updated successfully")
except Exception as e:
Expand Down Expand Up @@ -215,18 +231,18 @@ def get_album_images(
"album_id": album[0],
"album_name": album[1],
"description": album[2],
"is_hidden": bool(album[3]),
"is_locked": bool(album[3]),
"password_hash": album[4],
}

if album_dict["is_hidden"]:
if album_dict["is_locked"]:
if not body.password:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ErrorResponse(
success=False,
error="Password Required",
message="Password is required to access this hidden album.",
message="Password is required to access this locked album.",
).model_dump(),
)
if not verify_album_password(album_id, body.password):
Expand Down Expand Up @@ -355,3 +371,60 @@ def remove_images_from_album(
success=False, error="Failed to Remove Images", message=str(e)
).model_dump(),
)


# PUT /albums/{album_id}/cover - Set album cover image
@router.put("/{album_id}/cover", response_model=SuccessResponse)
def set_album_cover_image(
album_id: str = Path(...), body: SetCoverImageRequest = Body(...)
):
"""Set or update the cover image for an album"""
album = db_get_album(album_id)
if not album:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ErrorResponse(
success=False,
error="Album Not Found",
message="No album exists with the provided ID.",
).model_dump(),
)

# Verify the image exists in the album
album_image_ids = db_get_album_images(album_id)
if body.image_id not in album_image_ids:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="Image Not In Album",
message="The specified image is not in this album.",
).model_dump(),
)

try:
# Get the image path from the database
image_path = db_get_image_path(body.image_id)

if not image_path:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ErrorResponse(
success=False,
error="Image Not Found",
message="The specified image does not exist.",
).model_dump(),
)

db_update_album_cover_image(album_id, image_path)
Comment thread
SiddharthJiyani marked this conversation as resolved.

return SuccessResponse(
success=True, msg="Album cover image updated successfully"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ErrorResponse(
success=False, error="Failed to Set Cover Image", message=str(e)
).model_dump(),
)
Loading
Loading