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:
parent
bca01a5ac3
commit
a8fd0568c9
@ -46,6 +46,8 @@ export default function Search() {
|
||||
const [matchAll, setMatchAll] = useState(false)
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
const [dateTakenFrom, setDateTakenFrom] = useState('')
|
||||
const [dateTakenTo, setDateTakenTo] = useState('')
|
||||
const [mediaType, setMediaType] = useState<string>('all') // Default to 'all'
|
||||
|
||||
// Person autocomplete
|
||||
@ -136,6 +138,8 @@ export default function Search() {
|
||||
if (state.matchAll !== undefined) setMatchAll(state.matchAll)
|
||||
if (state.dateFrom) setDateFrom(state.dateFrom)
|
||||
if (state.dateTo) setDateTo(state.dateTo)
|
||||
if (state.dateTakenFrom) setDateTakenFrom(state.dateTakenFrom)
|
||||
if (state.dateTakenTo) setDateTakenTo(state.dateTakenTo)
|
||||
if (state.mediaType) setMediaType(state.mediaType)
|
||||
if (state.selectedPeople) setSelectedPeople(state.selectedPeople)
|
||||
if (state.inputValue) setInputValue(state.inputValue)
|
||||
@ -195,6 +199,8 @@ export default function Search() {
|
||||
matchAll,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
dateTakenFrom,
|
||||
dateTakenTo,
|
||||
mediaType,
|
||||
selectedPeople,
|
||||
inputValue,
|
||||
@ -211,7 +217,7 @@ export default function Search() {
|
||||
} catch (error) {
|
||||
console.error('Error saving search state:', error)
|
||||
}
|
||||
}, [searchType, selectedTags, matchAll, dateFrom, dateTo, mediaType, selectedPeople, inputValue, tagsExpanded, filtersExpanded, configExpanded, sortColumn, sortDir, page, results, total])
|
||||
}, [searchType, selectedTags, matchAll, dateFrom, dateTo, dateTakenFrom, dateTakenTo, mediaType, selectedPeople, inputValue, tagsExpanded, filtersExpanded, configExpanded, sortColumn, sortDir, page, results, total])
|
||||
|
||||
const performSearch = async (pageNum: number = page, showValidationErrors: boolean = true, searchTypeOverride?: SearchType) => {
|
||||
setLoading(true)
|
||||
@ -254,6 +260,11 @@ export default function Search() {
|
||||
return
|
||||
}
|
||||
params.person_name = allNames.join(', ')
|
||||
// Add tags filter if provided
|
||||
if (selectedTags.length > 0) {
|
||||
params.tag_names = selectedTags.join(', ')
|
||||
params.match_all = matchAll
|
||||
}
|
||||
} else if (currentSearchType === 'date') {
|
||||
if (!dateFrom && !dateTo) {
|
||||
if (showValidationErrors) {
|
||||
@ -264,7 +275,20 @@ export default function Search() {
|
||||
}
|
||||
params.date_from = dateFrom || undefined
|
||||
params.date_to = dateTo || undefined
|
||||
} else if (currentSearchType === 'tags') {
|
||||
// Add tags filter if provided
|
||||
if (selectedTags.length > 0) {
|
||||
params.tag_names = selectedTags.join(', ')
|
||||
params.match_all = matchAll
|
||||
}
|
||||
} else {
|
||||
// For non-date search types, apply date taken filter if provided
|
||||
if (dateTakenFrom || dateTakenTo) {
|
||||
params.date_from = dateTakenFrom || undefined
|
||||
params.date_to = dateTakenTo || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSearchType === 'tags') {
|
||||
if (selectedTags.length === 0) {
|
||||
if (showValidationErrors) {
|
||||
alert('Please select at least one tag to search for.')
|
||||
@ -369,8 +393,8 @@ export default function Search() {
|
||||
if (searchType === 'no_faces' || searchType === 'no_tags' || searchType === 'processed' || searchType === 'unprocessed' || searchType === 'favorites') {
|
||||
handleSearch()
|
||||
}
|
||||
// Clear selected tags when switching away from tag search
|
||||
if (searchType !== 'tags') {
|
||||
// Clear selected tags when switching away from tag/name/date search types
|
||||
if (searchType !== 'tags' && searchType !== 'name' && searchType !== 'date') {
|
||||
setSelectedTags([])
|
||||
}
|
||||
// Clear person autocomplete when switching away from name search
|
||||
@ -808,10 +832,28 @@ export default function Search() {
|
||||
if (allNames.length > 0) {
|
||||
baseParams.person_name = allNames.join(', ')
|
||||
}
|
||||
// Add tags filter if provided
|
||||
if (selectedTags.length > 0) {
|
||||
baseParams.tag_names = selectedTags.join(', ')
|
||||
baseParams.match_all = matchAll
|
||||
}
|
||||
} else if (searchType === 'date') {
|
||||
baseParams.date_from = dateFrom || undefined
|
||||
baseParams.date_to = dateTo || undefined
|
||||
} else if (searchType === 'tags') {
|
||||
// Add tags filter if provided
|
||||
if (selectedTags.length > 0) {
|
||||
baseParams.tag_names = selectedTags.join(', ')
|
||||
baseParams.match_all = matchAll
|
||||
}
|
||||
} else {
|
||||
// For non-date search types, apply date taken filter if provided
|
||||
if (dateTakenFrom || dateTakenTo) {
|
||||
baseParams.date_from = dateTakenFrom || undefined
|
||||
baseParams.date_to = dateTakenTo || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (searchType === 'tags') {
|
||||
baseParams.tag_names = selectedTags.join(', ')
|
||||
baseParams.match_all = matchAll
|
||||
}
|
||||
@ -994,34 +1036,301 @@ export default function Search() {
|
||||
<p className="text-xs text-gray-500">
|
||||
Select people from the dropdown or type names manually. Separate multiple names with commas.
|
||||
</p>
|
||||
|
||||
{/* Tags filter for name search */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-medium text-gray-700">Filter by tags (optional)</h2>
|
||||
<button
|
||||
onClick={() => setTagsExpanded(!tagsExpanded)}
|
||||
className="text-lg text-gray-600 hover:text-gray-800"
|
||||
title={tagsExpanded ? 'Collapse tags section' : 'Expand tags section'}
|
||||
aria-label={tagsExpanded ? 'Collapse tags section' : 'Expand tags section'}
|
||||
>
|
||||
{tagsExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
{tagsExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div>
|
||||
<label htmlFor="tag-search-input-name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Add tags
|
||||
</label>
|
||||
{/* Selected tags display */}
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{selectedTags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTags(selectedTags.filter(t => t !== tag))
|
||||
}}
|
||||
className="hover:text-blue-600"
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedTags([])
|
||||
}}
|
||||
className="text-xs text-gray-600 hover:text-gray-800 underline"
|
||||
title="Clear all selected tags"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Tag input and dropdown */}
|
||||
<div className="relative">
|
||||
<input
|
||||
id="tag-search-input-name"
|
||||
ref={tagInputRef}
|
||||
type="text"
|
||||
value={tagSearchInput}
|
||||
onChange={(e) => {
|
||||
setTagSearchInput(e.target.value)
|
||||
setShowTagDropdown(true)
|
||||
}}
|
||||
onFocus={() => setShowTagDropdown(true)}
|
||||
onBlur={(e) => {
|
||||
setTimeout(() => {
|
||||
if (!tagDropdownRef.current?.contains(e.relatedTarget as Node)) {
|
||||
setShowTagDropdown(false)
|
||||
}
|
||||
}, 200)
|
||||
}}
|
||||
placeholder="Type to search and select tags..."
|
||||
className="w-full border rounded px-3 py-2 text-sm"
|
||||
/>
|
||||
{showTagDropdown && (
|
||||
<div
|
||||
ref={tagDropdownRef}
|
||||
className="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg max-h-60 overflow-auto"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{(() => {
|
||||
const filtered = availableTags.filter(tag =>
|
||||
tag.tag_name.toLowerCase().includes(tagSearchInput.toLowerCase()) &&
|
||||
!selectedTags.includes(tag.tag_name)
|
||||
)
|
||||
return filtered.length > 0 ? (
|
||||
filtered.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => {
|
||||
if (!selectedTags.includes(tag.tag_name)) {
|
||||
setSelectedTags([...selectedTags, tag.tag_name])
|
||||
}
|
||||
setTagSearchInput('')
|
||||
setTimeout(() => {
|
||||
setShowTagDropdown(true)
|
||||
tagInputRef.current?.focus()
|
||||
}, 0)
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-gray-100 cursor-pointer text-sm"
|
||||
>
|
||||
{tag.tag_name}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-2 text-gray-500 text-sm">
|
||||
{tagSearchInput.trim() ? 'No matching tags found' : 'Start typing to search for tags'}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="match-mode-select-name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Match mode
|
||||
</label>
|
||||
<select
|
||||
id="match-mode-select-name"
|
||||
value={matchAll ? 'ALL' : 'ANY'}
|
||||
onChange={(e) => setMatchAll(e.target.value === 'ALL')}
|
||||
className="border rounded px-3 py-2"
|
||||
>
|
||||
<option value="ANY">Match any tag (photos with at least one selected tag)</option>
|
||||
<option value="ALL">Match all tags (photos with all selected tags)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchType === 'date' && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label htmlFor="date-from-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Start date
|
||||
</label>
|
||||
<input
|
||||
id="date-from-input"
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end gap-4">
|
||||
<div>
|
||||
<label htmlFor="date-from-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Start date
|
||||
</label>
|
||||
<input
|
||||
id="date-from-input"
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="date-to-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
End date <span className="text-gray-500 font-normal">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="date-to-input"
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="date-to-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
End date <span className="text-gray-500 font-normal">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="date-to-input"
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
{/* Tags filter for date search */}
|
||||
<div className="w-1/2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-medium text-gray-700">Filter by tags (optional)</h2>
|
||||
<button
|
||||
onClick={() => setTagsExpanded(!tagsExpanded)}
|
||||
className="text-lg text-gray-600 hover:text-gray-800"
|
||||
title={tagsExpanded ? 'Collapse tags section' : 'Expand tags section'}
|
||||
aria-label={tagsExpanded ? 'Collapse tags section' : 'Expand tags section'}
|
||||
>
|
||||
{tagsExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
{tagsExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div>
|
||||
<label htmlFor="tag-search-input-date" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Add tags
|
||||
</label>
|
||||
{/* Selected tags display */}
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{selectedTags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTags(selectedTags.filter(t => t !== tag))
|
||||
}}
|
||||
className="hover:text-blue-600"
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedTags([])
|
||||
}}
|
||||
className="text-xs text-gray-600 hover:text-gray-800 underline"
|
||||
title="Clear all selected tags"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Tag input and dropdown */}
|
||||
<div className="relative">
|
||||
<input
|
||||
id="tag-search-input-date"
|
||||
ref={tagInputRef}
|
||||
type="text"
|
||||
value={tagSearchInput}
|
||||
onChange={(e) => {
|
||||
setTagSearchInput(e.target.value)
|
||||
setShowTagDropdown(true)
|
||||
}}
|
||||
onFocus={() => setShowTagDropdown(true)}
|
||||
onBlur={(e) => {
|
||||
setTimeout(() => {
|
||||
if (!tagDropdownRef.current?.contains(e.relatedTarget as Node)) {
|
||||
setShowTagDropdown(false)
|
||||
}
|
||||
}, 200)
|
||||
}}
|
||||
placeholder="Type to search and select tags..."
|
||||
className="w-full border rounded px-3 py-2 text-sm"
|
||||
/>
|
||||
{showTagDropdown && (
|
||||
<div
|
||||
ref={tagDropdownRef}
|
||||
className="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg max-h-60 overflow-auto"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{(() => {
|
||||
const filtered = availableTags.filter(tag =>
|
||||
tag.tag_name.toLowerCase().includes(tagSearchInput.toLowerCase()) &&
|
||||
!selectedTags.includes(tag.tag_name)
|
||||
)
|
||||
return filtered.length > 0 ? (
|
||||
filtered.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => {
|
||||
if (!selectedTags.includes(tag.tag_name)) {
|
||||
setSelectedTags([...selectedTags, tag.tag_name])
|
||||
}
|
||||
setTagSearchInput('')
|
||||
setTimeout(() => {
|
||||
setShowTagDropdown(true)
|
||||
tagInputRef.current?.focus()
|
||||
}, 0)
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-gray-100 cursor-pointer text-sm"
|
||||
>
|
||||
{tag.tag_name}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-2 text-gray-500 text-sm">
|
||||
{tagSearchInput.trim() ? 'No matching tags found' : 'Start typing to search for tags'}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="match-mode-select-date" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Match mode
|
||||
</label>
|
||||
<select
|
||||
id="match-mode-select-date"
|
||||
value={matchAll ? 'ALL' : 'ANY'}
|
||||
onChange={(e) => setMatchAll(e.target.value === 'ALL')}
|
||||
className="border rounded px-3 py-2"
|
||||
>
|
||||
<option value="ANY">Match any tag (photos with at least one selected tag)</option>
|
||||
<option value="ALL">Match all tags (photos with all selected tags)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -1179,20 +1488,68 @@ export default function Search() {
|
||||
</div>
|
||||
{filtersExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div>
|
||||
<label htmlFor="media-type-select" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Media type
|
||||
</label>
|
||||
<select
|
||||
id="media-type-select"
|
||||
value={mediaType}
|
||||
onChange={(e) => setMediaType(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
>
|
||||
<option value="all">All media types</option>
|
||||
<option value="image">Photos only</option>
|
||||
<option value="video">Videos only</option>
|
||||
</select>
|
||||
<div className="flex items-end gap-4">
|
||||
<div>
|
||||
<label htmlFor="media-type-select" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Media type
|
||||
</label>
|
||||
<select
|
||||
id="media-type-select"
|
||||
value={mediaType}
|
||||
onChange={(e) => setMediaType(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
>
|
||||
<option value="all">All media types</option>
|
||||
<option value="image">Photos only</option>
|
||||
<option value="video">Videos only</option>
|
||||
</select>
|
||||
</div>
|
||||
{searchType !== 'date' && (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="date-taken-from-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Date Taken - From
|
||||
</label>
|
||||
<input
|
||||
id="date-taken-from-input"
|
||||
type="date"
|
||||
value={dateTakenFrom}
|
||||
onChange={(e) => setDateTakenFrom(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="date-taken-to-input" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Date Taken - To <span className="text-gray-500 font-normal">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="date-taken-to-input"
|
||||
type="date"
|
||||
value={dateTakenTo}
|
||||
onChange={(e) => setDateTakenTo(e.target.value)}
|
||||
className="w-48 border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
{(dateTakenFrom || dateTakenTo) && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDateTakenFrom('')
|
||||
setDateTakenTo('')
|
||||
}}
|
||||
className="px-3 py-2 text-sm text-gray-600 hover:text-gray-800 border rounded hover:bg-gray-50"
|
||||
title="Clear date filters"
|
||||
>
|
||||
Clear dates
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user