feat: Enhance Search component with date taken filters and tag management

This commit adds new date taken filters to the Search component, allowing users to specify a date range for photos based on when they were taken. Additionally, the tagging functionality is improved with options to filter by tags during searches, including a match mode for tag selection. The UI is updated to accommodate these features, enhancing user experience and search capabilities. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-12-11 13:39:36 -05:00
parent bca01a5ac3
commit a8fd0568c9
3 changed files with 537 additions and 54 deletions
+18 -12
View File
@@ -99,6 +99,15 @@ def search_photos(
).first()
return favorite is not None
# Parse date filters for use as additional filters (when not using date search type)
df = date.fromisoformat(date_from) if date_from else None
dt = date.fromisoformat(date_to) if date_to else None
# Parse tag filters for use as additional filters
tag_list = None
if tag_names:
tag_list = [t.strip() for t in tag_names.split(",") if t.strip()]
if search_type == "name":
if not person_name:
raise HTTPException(
@@ -106,7 +115,7 @@ def search_photos(
detail="person_name is required for name search",
)
results, total = search_photos_by_name(
db, person_name, folder_path, media_type, page, page_size
db, person_name, folder_path, media_type, df, dt, tag_list, match_all, page, page_size
)
for photo, full_name in results:
tags = get_photo_tags(db, photo.id)
@@ -134,9 +143,7 @@ def search_photos(
status_code=status.HTTP_400_BAD_REQUEST,
detail="At least one of date_from or date_to is required",
)
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, media_type, page, page_size)
results, total = search_photos_by_date(db, df, dt, folder_path, media_type, tag_list, match_all, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -164,14 +171,13 @@ def search_photos(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tag_names is required for tag search",
)
tag_list = [t.strip() for t in tag_names.split(",") if t.strip()]
if not tag_list:
if not tag_list or len(tag_list) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="At least one tag name is required",
)
results, total = search_photos_by_tags(
db, tag_list, match_all, folder_path, media_type, page, page_size
db, tag_list, match_all, folder_path, media_type, df, dt, page, page_size
)
for photo in results:
tags = get_photo_tags(db, photo.id)
@@ -195,7 +201,7 @@ def search_photos(
)
)
elif search_type == "no_faces":
results, total = get_photos_without_faces(db, folder_path, media_type, page, page_size)
results, total = get_photos_without_faces(db, folder_path, media_type, df, dt, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
# Convert datetime to date for date_added
@@ -216,7 +222,7 @@ def search_photos(
)
)
elif search_type == "no_tags":
results, total = get_photos_without_tags(db, folder_path, media_type, page, page_size)
results, total = get_photos_without_tags(db, folder_path, media_type, df, dt, 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)
@@ -238,7 +244,7 @@ def search_photos(
)
)
elif search_type == "processed":
results, total = get_processed_photos(db, folder_path, media_type, page, page_size)
results, total = get_processed_photos(db, folder_path, media_type, df, dt, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -261,7 +267,7 @@ def search_photos(
)
)
elif search_type == "unprocessed":
results, total = get_unprocessed_photos(db, folder_path, media_type, page, page_size)
results, total = get_unprocessed_photos(db, folder_path, media_type, df, dt, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
@@ -289,7 +295,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, media_type, page, page_size)
results, total = get_favorite_photos(db, username, folder_path, media_type, df, dt, page, page_size)
for photo in results:
tags = get_photo_tags(db, photo.id)
face_count = get_photo_face_count(db, photo.id)
+120
View File
@@ -16,6 +16,10 @@ def search_photos_by_name(
person_name: str,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
tag_names: Optional[List[str]] = None,
match_all: bool = False,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Tuple[Photo, str]], int]:
@@ -81,6 +85,42 @@ def search_photos_by_name(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Apply tag filter if provided
if tag_names:
# Find tag IDs (case-insensitive)
tag_ids = (
db.query(Tag.id)
.filter(func.lower(Tag.tag_name).in_([t.lower().strip() for t in tag_names]))
.all()
)
tag_ids = [tid[0] for tid in tag_ids]
if tag_ids:
if match_all:
# Photos that have ALL specified tags
query = (
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
.group_by(Photo.id, Person.id)
.having(func.count(func.distinct(PhotoTagLinkage.tag_id)) == len(tag_ids))
)
else:
# Photos that have ANY of the specified tags
tagged_photo_ids_subquery = (
db.query(PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
)
query = query.filter(Photo.id.in_(tagged_photo_ids_subquery))
else:
# No matching tags found - return empty result
return [], 0
# Total count
total = query.count()
@@ -102,6 +142,8 @@ def search_photos_by_date(
date_to: Optional[date] = None,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
tag_names: Optional[List[str]] = None,
match_all: bool = False,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -129,6 +171,36 @@ def search_photos_by_date(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply tag filter if provided
if tag_names:
# Find tag IDs (case-insensitive)
tag_ids = (
db.query(Tag.id)
.filter(func.lower(Tag.tag_name).in_([t.lower().strip() for t in tag_names]))
.all()
)
tag_ids = [tid[0] for tid in tag_ids]
if tag_ids:
if match_all:
# Photos that have ALL specified tags
query = (
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
.group_by(Photo.id)
.having(func.count(func.distinct(PhotoTagLinkage.tag_id)) == len(tag_ids))
)
else:
# Photos that have ANY of the specified tags
tagged_photo_ids_subquery = (
db.query(PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
)
query = query.filter(Photo.id.in_(tagged_photo_ids_subquery))
else:
# No matching tags found - return empty result
return [], 0
# Total count
total = query.count()
@@ -144,6 +216,8 @@ def search_photos_by_tags(
match_all: bool = False,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -196,6 +270,12 @@ def search_photos_by_tags(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Total count
total = query.count()
@@ -209,6 +289,8 @@ def get_photos_without_faces(
db: Session,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -234,6 +316,12 @@ def get_photos_without_faces(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Total count
total = query.count()
@@ -247,6 +335,8 @@ def get_photos_without_tags(
db: Session,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -270,6 +360,12 @@ def get_photos_without_tags(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Total count
total = query.count()
@@ -312,6 +408,8 @@ def get_processed_photos(
db: Session,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -331,6 +429,12 @@ def get_processed_photos(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Total count
total = query.count()
@@ -345,6 +449,8 @@ def get_favorite_photos(
username: str,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -368,6 +474,12 @@ def get_favorite_photos(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
total = query.count()
# Order by favorite date (most recent first), then date_taken
@@ -387,6 +499,8 @@ def get_unprocessed_photos(
db: Session,
folder_path: Optional[str] = None,
media_type: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Photo], int]:
@@ -406,6 +520,12 @@ def get_unprocessed_photos(
if media_type and media_type.lower() != "all":
query = query.filter(Photo.media_type == media_type.lower())
# Apply date taken filter if provided
if date_from:
query = query.filter(Photo.date_taken >= date_from)
if date_to:
query = query.filter(Photo.date_taken <= date_to)
# Total count
total = query.count()