Skip to content

Commit ff90128

Browse files
feat: implement albums feature with create, edit, delete, and image management UI
1 parent 8980d72 commit ff90128

File tree

23 files changed

+2319
-161
lines changed

23 files changed

+2319
-161
lines changed

backend/app/database/albums.py

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ def db_create_albums_table() -> None:
5050
album_id TEXT PRIMARY KEY,
5151
album_name TEXT UNIQUE,
5252
description TEXT,
53-
is_hidden BOOLEAN DEFAULT 0,
54-
password_hash TEXT
53+
is_locked BOOLEAN DEFAULT 0,
54+
password_hash TEXT,
55+
cover_image_path TEXT
5556
)
5657
"""
5758
)
@@ -61,6 +62,26 @@ def db_create_albums_table() -> None:
6162
conn.close()
6263

6364

65+
def db_migrate_add_cover_image_column() -> None:
66+
"""Add cover_image_path column to existing albums table if it doesn't exist"""
67+
conn = None
68+
try:
69+
conn = sqlite3.connect(DATABASE_PATH)
70+
cursor = conn.cursor()
71+
72+
# Check if column exists
73+
cursor.execute("PRAGMA table_info(albums)")
74+
columns = [column[1] for column in cursor.fetchall()]
75+
76+
if "cover_image_path" not in columns:
77+
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
78+
conn.commit()
79+
print("Added cover_image_path column to albums table")
80+
finally:
81+
if conn is not None:
82+
conn.close()
83+
84+
6485
def db_create_album_images_table() -> None:
6586
conn = None
6687
try:
@@ -83,14 +104,15 @@ def db_create_album_images_table() -> None:
83104
conn.close()
84105

85106

86-
def db_get_all_albums(show_hidden: bool = False):
107+
def db_get_all_albums(show_locked: bool = True):
108+
"""Get all albums. By default, returns all albums including locked ones."""
87109
conn = sqlite3.connect(DATABASE_PATH)
88110
cursor = conn.cursor()
89111
try:
90-
if show_hidden:
91-
cursor.execute("SELECT * FROM albums")
92-
else:
93-
cursor.execute("SELECT * FROM albums WHERE is_hidden = 0")
112+
# Always show all albums (locked and unlocked)
113+
cursor.execute(
114+
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums"
115+
)
94116
albums = cursor.fetchall()
95117
return albums
96118
finally:
@@ -101,7 +123,10 @@ def db_get_album_by_name(name: str):
101123
conn = sqlite3.connect(DATABASE_PATH)
102124
cursor = conn.cursor()
103125
try:
104-
cursor.execute("SELECT * FROM albums WHERE album_name = ?", (name,))
126+
cursor.execute(
127+
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums WHERE album_name = ?",
128+
(name,),
129+
)
105130
album = cursor.fetchone()
106131
return album if album else None
107132
finally:
@@ -112,7 +137,10 @@ def db_get_album(album_id: str):
112137
conn = sqlite3.connect(DATABASE_PATH)
113138
cursor = conn.cursor()
114139
try:
115-
cursor.execute("SELECT * FROM albums WHERE album_id = ?", (album_id,))
140+
cursor.execute(
141+
"SELECT album_id, album_name, description, is_locked, password_hash, cover_image_path FROM albums WHERE album_id = ?",
142+
(album_id,),
143+
)
116144
album = cursor.fetchone()
117145
return album if album else None
118146
finally:
@@ -123,7 +151,7 @@ def db_insert_album(
123151
album_id: str,
124152
album_name: str,
125153
description: str = "",
126-
is_hidden: bool = False,
154+
is_locked: bool = False,
127155
password: str = None,
128156
):
129157
conn = sqlite3.connect(DATABASE_PATH)
@@ -136,10 +164,10 @@ def db_insert_album(
136164
).decode("utf-8")
137165
cursor.execute(
138166
"""
139-
INSERT INTO albums (album_id, album_name, description, is_hidden, password_hash)
167+
INSERT INTO albums (album_id, album_name, description, is_locked, password_hash)
140168
VALUES (?, ?, ?, ?, ?)
141169
""",
142-
(album_id, album_name, description, int(is_hidden), password_hash),
170+
(album_id, album_name, description, int(is_locked), password_hash),
143171
)
144172
conn.commit()
145173
finally:
@@ -150,7 +178,7 @@ def db_update_album(
150178
album_id: str,
151179
album_name: str,
152180
description: str,
153-
is_hidden: bool,
181+
is_locked: bool,
154182
password: str = None,
155183
):
156184
conn = sqlite3.connect(DATABASE_PATH)
@@ -164,20 +192,20 @@ def db_update_album(
164192
cursor.execute(
165193
"""
166194
UPDATE albums
167-
SET album_name = ?, description = ?, is_hidden = ?, password_hash = ?
195+
SET album_name = ?, description = ?, is_locked = ?, password_hash = ?
168196
WHERE album_id = ?
169197
""",
170-
(album_name, description, int(is_hidden), password_hash, album_id),
198+
(album_name, description, int(is_locked), password_hash, album_id),
171199
)
172200
else:
173201
# Update without changing password
174202
cursor.execute(
175203
"""
176204
UPDATE albums
177-
SET album_name = ?, description = ?, is_hidden = ?
205+
SET album_name = ?, description = ?, is_locked = ?
178206
WHERE album_id = ?
179207
""",
180-
(album_name, description, int(is_hidden), album_id),
208+
(album_name, description, int(is_locked), album_id),
181209
)
182210
conn.commit()
183211
finally:
@@ -190,6 +218,20 @@ def db_delete_album(album_id: str):
190218
cursor.execute("DELETE FROM albums WHERE album_id = ?", (album_id,))
191219

192220

221+
def db_update_album_cover_image(album_id: str, cover_image_path: str):
222+
"""Update the cover image path for an album"""
223+
conn = sqlite3.connect(DATABASE_PATH)
224+
cursor = conn.cursor()
225+
try:
226+
cursor.execute(
227+
"UPDATE albums SET cover_image_path = ? WHERE album_id = ?",
228+
(cover_image_path, album_id),
229+
)
230+
conn.commit()
231+
finally:
232+
conn.close()
233+
234+
193235
def db_get_album_images(album_id: str):
194236
conn = sqlite3.connect(DATABASE_PATH)
195237
cursor = conn.cursor()

backend/app/routes/albums.py

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, HTTPException, status, Query, Body, Path
1+
from fastapi import APIRouter, HTTPException, status, Body, Path
22
import uuid
33
from app.schemas.album import (
44
GetAlbumsResponse,
@@ -11,6 +11,7 @@
1111
SuccessResponse,
1212
ErrorResponse,
1313
ImageIdsRequest,
14+
SetCoverImageRequest,
1415
Album,
1516
)
1617
from app.database.albums import (
@@ -24,24 +25,32 @@
2425
db_add_images_to_album,
2526
db_remove_image_from_album,
2627
db_remove_images_from_album,
28+
db_update_album_cover_image,
2729
verify_album_password,
2830
)
2931

3032
router = APIRouter()
3133

3234

33-
# GET /albums/ - Get all albums
35+
# GET /albums/ - Get all albums (including locked ones)
3436
@router.get("/", response_model=GetAlbumsResponse)
35-
def get_albums(show_hidden: bool = Query(False)):
36-
albums = db_get_all_albums(show_hidden)
37+
def get_albums():
38+
"""Get all albums. Always returns both locked and unlocked albums."""
39+
albums = db_get_all_albums()
3740
album_list = []
3841
for album in albums:
42+
# Get image count for each album
43+
image_ids = db_get_album_images(album[0])
44+
image_count = len(image_ids)
45+
3946
album_list.append(
4047
Album(
4148
album_id=album[0],
4249
album_name=album[1],
4350
description=album[2] or "",
44-
is_hidden=bool(album[3]),
51+
is_locked=bool(album[3]),
52+
cover_image_path=album[5] if len(album) > 5 else None,
53+
image_count=image_count,
4554
)
4655
)
4756
return GetAlbumsResponse(success=True, albums=album_list)
@@ -64,7 +73,7 @@ def create_album(body: CreateAlbumRequest):
6473
album_id = str(uuid.uuid4())
6574
try:
6675
db_insert_album(
67-
album_id, body.name, body.description, body.is_hidden, body.password
76+
album_id, body.name, body.description, body.is_locked, body.password
6877
)
6978
return CreateAlbumResponse(success=True, album_id=album_id)
7079
except Exception as e:
@@ -91,11 +100,17 @@ def get_album(album_id: str = Path(...)):
91100
)
92101

93102
try:
103+
# Get image count for the album
104+
image_ids = db_get_album_images(album_id)
105+
image_count = len(image_ids)
106+
94107
album_obj = Album(
95108
album_id=album[0],
96109
album_name=album[1],
97110
description=album[2] or "",
98-
is_hidden=bool(album[3]),
111+
is_locked=bool(album[3]),
112+
cover_image_path=album[5] if len(album) > 5 else None,
113+
image_count=image_count,
99114
)
100115
return GetAlbumResponse(success=True, data=album_obj)
101116
except Exception as e:
@@ -109,6 +124,8 @@ def get_album(album_id: str = Path(...)):
109124
)
110125

111126

127+
# PUT /albums/{album_id} - Update Album
128+
@router.put("/{album_id}", response_model=SuccessResponse)
112129
# PUT /albums/{album_id} - Update Album
113130
@router.put("/{album_id}", response_model=SuccessResponse)
114131
def update_album(album_id: str = Path(...), body: UpdateAlbumRequest = Body(...)):
@@ -127,11 +144,11 @@ def update_album(album_id: str = Path(...), body: UpdateAlbumRequest = Body(...)
127144
"album_id": album[0],
128145
"album_name": album[1],
129146
"description": album[2],
130-
"is_hidden": bool(album[3]),
147+
"is_locked": bool(album[3]),
131148
"password_hash": album[4],
132149
}
133150

134-
if album_dict["password_hash"]:
151+
if album_dict["is_locked"]:
135152
if not body.current_password:
136153
raise HTTPException(
137154
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -154,7 +171,7 @@ def update_album(album_id: str = Path(...), body: UpdateAlbumRequest = Body(...)
154171

155172
try:
156173
db_update_album(
157-
album_id, body.name, body.description, body.is_hidden, body.password
174+
album_id, body.name, body.description, body.is_locked, body.password
158175
)
159176
return SuccessResponse(success=True, msg="Album updated successfully")
160177
except Exception as e:
@@ -215,18 +232,18 @@ def get_album_images(
215232
"album_id": album[0],
216233
"album_name": album[1],
217234
"description": album[2],
218-
"is_hidden": bool(album[3]),
235+
"is_locked": bool(album[3]),
219236
"password_hash": album[4],
220237
}
221238

222-
if album_dict["is_hidden"]:
239+
if album_dict["is_locked"]:
223240
if not body.password:
224241
raise HTTPException(
225242
status_code=status.HTTP_401_UNAUTHORIZED,
226243
detail=ErrorResponse(
227244
success=False,
228245
error="Password Required",
229-
message="Password is required to access this hidden album.",
246+
message="Password is required to access this locked album.",
230247
).model_dump(),
231248
)
232249
if not verify_album_password(album_id, body.password):
@@ -355,3 +372,68 @@ def remove_images_from_album(
355372
success=False, error="Failed to Remove Images", message=str(e)
356373
).model_dump(),
357374
)
375+
376+
377+
# PUT /albums/{album_id}/cover - Set album cover image
378+
@router.put("/{album_id}/cover", response_model=SuccessResponse)
379+
def set_album_cover_image(
380+
album_id: str = Path(...), body: SetCoverImageRequest = Body(...)
381+
):
382+
"""Set or update the cover image for an album"""
383+
album = db_get_album(album_id)
384+
if not album:
385+
raise HTTPException(
386+
status_code=status.HTTP_404_NOT_FOUND,
387+
detail=ErrorResponse(
388+
success=False,
389+
error="Album Not Found",
390+
message="No album exists with the provided ID.",
391+
).model_dump(),
392+
)
393+
394+
# Verify the image exists in the album
395+
album_image_ids = db_get_album_images(album_id)
396+
if body.image_id not in album_image_ids:
397+
raise HTTPException(
398+
status_code=status.HTTP_400_BAD_REQUEST,
399+
detail=ErrorResponse(
400+
success=False,
401+
error="Image Not In Album",
402+
message="The specified image is not in this album.",
403+
).model_dump(),
404+
)
405+
406+
try:
407+
# Get the image path from the database
408+
import sqlite3
409+
from app.config.settings import DATABASE_PATH
410+
411+
conn = sqlite3.connect(DATABASE_PATH)
412+
cursor = conn.cursor()
413+
cursor.execute("SELECT path FROM images WHERE id = ?", (body.image_id,))
414+
result = cursor.fetchone()
415+
conn.close()
416+
417+
if not result:
418+
raise HTTPException(
419+
status_code=status.HTTP_404_NOT_FOUND,
420+
detail=ErrorResponse(
421+
success=False,
422+
error="Image Not Found",
423+
message="The specified image does not exist.",
424+
).model_dump(),
425+
)
426+
427+
image_path = result[0]
428+
db_update_album_cover_image(album_id, image_path)
429+
430+
return SuccessResponse(
431+
success=True, msg="Album cover image updated successfully"
432+
)
433+
except Exception as e:
434+
raise HTTPException(
435+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
436+
detail=ErrorResponse(
437+
success=False, error="Failed to Set Cover Image", message=str(e)
438+
).model_dump(),
439+
)

0 commit comments

Comments
 (0)