-
User Tagged Photos
Review tags suggested by users. Approving creates/links the tag to the selected photo.
diff --git a/src/web/api/photos.py b/src/web/api/photos.py
index 467e728..c206497 100644
--- a/src/web/api/photos.py
+++ b/src/web/api/photos.py
@@ -66,6 +66,7 @@ def search_photos(
date_from: Optional[str] = Query(None, description="Date from (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Date to (YYYY-MM-DD)"),
folder_path: Optional[str] = Query(None, description="Filter by folder path"),
+ media_type: Optional[str] = Query(None, description="Filter by media type: 'all', 'image', or 'video'"),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
@@ -105,7 +106,7 @@ def search_photos(
detail="person_name is required for name search",
)
results, total = search_photos_by_name(
- db, person_name, folder_path, page, page_size
+ db, person_name, folder_path, media_type, page, page_size
)
for photo, full_name in results:
tags = get_photo_tags(db, photo.id)
@@ -135,7 +136,7 @@ def search_photos(
)
df = date.fromisoformat(date_from) if date_from else None
dt = date.fromisoformat(date_to) if date_to else None
- results, total = search_photos_by_date(db, df, dt, folder_path, page, page_size)
+ results, total = search_photos_by_date(db, df, dt, folder_path, media_type, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -170,7 +171,7 @@ def search_photos(
detail="At least one tag name is required",
)
results, total = search_photos_by_tags(
- db, tag_list, match_all, folder_path, page, page_size
+ db, tag_list, match_all, folder_path, media_type, page, page_size
)
for photo in results:
tags = get_photo_tags(db, photo.id)
@@ -194,7 +195,7 @@ def search_photos(
)
)
elif search_type == "no_faces":
- results, total = get_photos_without_faces(db, folder_path, page, page_size)
+ results, total = get_photos_without_faces(db, folder_path, media_type, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
# Convert datetime to date for date_added
@@ -215,7 +216,7 @@ def search_photos(
)
)
elif search_type == "no_tags":
- results, total = get_photos_without_tags(db, folder_path, page, page_size)
+ results, total = get_photos_without_tags(db, folder_path, media_type, page, page_size)
for photo in results:
face_count = get_photo_face_count(db, photo.id)
person_name_val = get_photo_person(db, photo.id)
@@ -237,7 +238,7 @@ def search_photos(
)
)
elif search_type == "processed":
- results, total = get_processed_photos(db, folder_path, page, page_size)
+ results, total = get_processed_photos(db, folder_path, media_type, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -260,7 +261,7 @@ def search_photos(
)
)
elif search_type == "unprocessed":
- results, total = get_unprocessed_photos(db, folder_path, page, page_size)
+ results, total = get_unprocessed_photos(db, folder_path, media_type, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -288,7 +289,7 @@ def search_photos(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required for favorites search",
)
- results, total = get_favorite_photos(db, username, folder_path, page, page_size)
+ results, total = get_favorite_photos(db, username, folder_path, media_type, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
diff --git a/src/web/services/search_service.py b/src/web/services/search_service.py
index 9d1111e..d262cb0 100644
--- a/src/web/services/search_service.py
+++ b/src/web/services/search_service.py
@@ -15,6 +15,7 @@ def search_photos_by_name(
db: Session,
person_name: str,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Tuple[Photo, str]], int]:
@@ -26,6 +27,7 @@ def search_photos_by_name(
- Searches first_name, last_name, middle_name, maiden_name
- Returns (photo, full_name) tuples
- Filters by folder_path if provided
+ - Filters by media_type if provided ("image" or "video", None/"all" for all)
- Multiple names: comma-separated, searches for photos with ANY matching person
"""
search_name = (person_name or "").strip()
@@ -75,6 +77,10 @@ def search_photos_by_name(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -95,6 +101,7 @@ def search_photos_by_date(
date_from: Optional[date] = None,
date_to: Optional[date] = None,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -103,6 +110,7 @@ def search_photos_by_date(
Matches desktop behavior exactly:
- Filters by date_taken
- Requires at least one date
+ - Filters by media_type if provided ("image" or "video", None/"all" for all)
- Returns photos ordered by date_taken DESC
"""
query = db.query(Photo).filter(Photo.date_taken.is_not(None))
@@ -117,6 +125,10 @@ def search_photos_by_date(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -131,6 +143,7 @@ def search_photos_by_tags(
tag_names: List[str],
match_all: bool = False,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -140,6 +153,7 @@ def search_photos_by_tags(
- match_all=True: photos must have ALL tags
- match_all=False: photos with ANY tag
- Case-insensitive tag matching
+ - Filters by media_type if provided ("image" or "video", None/"all" for all)
"""
if not tag_names:
return [], 0
@@ -178,6 +192,10 @@ def search_photos_by_tags(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -190,12 +208,14 @@ def search_photos_by_tags(
def get_photos_without_faces(
db: Session,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
"""Get photos that have no detected faces.
Only includes processed photos (photos that have been processed for face detection).
+ Filters by media_type if provided ("image" or "video", None/"all" for all).
Matches desktop behavior exactly.
"""
query = (
@@ -210,6 +230,10 @@ def get_photos_without_faces(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -222,11 +246,13 @@ def get_photos_without_faces(
def get_photos_without_tags(
db: Session,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
"""Get photos that have no tags.
+ Filters by media_type if provided ("image" or "video", None/"all" for all).
Matches desktop behavior exactly.
"""
query = (
@@ -240,6 +266,10 @@ def get_photos_without_tags(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -281,11 +311,13 @@ def get_photo_face_count(db: Session, photo_id: int) -> int:
def get_processed_photos(
db: Session,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
"""Get photos that have been processed for face detection.
+ Filters by media_type if provided ("image" or "video", None/"all" for all).
Matches desktop behavior exactly.
"""
query = db.query(Photo).filter(Photo.processed == True) # noqa: E712
@@ -295,6 +327,10 @@ def get_processed_photos(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()
@@ -308,10 +344,14 @@ def get_favorite_photos(
db: Session,
username: str,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
- """Get all favorite photos for a user with pagination."""
+ """Get all favorite photos for a user with pagination.
+
+ Filters by media_type if provided ("image" or "video", None/"all" for all).
+ """
from src.web.db.models import PhotoFavorite
# Join favorites with photos
@@ -324,6 +364,10 @@ def get_favorite_photos(
if folder_path:
query = query.filter(Photo.path.like(f"{folder_path}%"))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
total = query.count()
# Order by favorite date (most recent first), then date_taken
@@ -342,11 +386,13 @@ def get_favorite_photos(
def get_unprocessed_photos(
db: Session,
folder_path: Optional[str] = None,
+ media_type: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
"""Get photos that have not been processed for face detection.
+ Filters by media_type if provided ("image" or "video", None/"all" for all).
Matches desktop behavior exactly.
"""
query = db.query(Photo).filter(Photo.processed == False) # noqa: E712
@@ -356,6 +402,10 @@ def get_unprocessed_photos(
folder_path = folder_path.strip()
query = query.filter(Photo.path.startswith(folder_path))
+ # Apply media type filter if provided
+ if media_type and media_type.lower() != "all":
+ query = query.filter(Photo.media_type == media_type.lower())
+
# Total count
total = query.count()