diff --git a/frontend/src/api/pendingLinkages.ts b/frontend/src/api/pendingLinkages.ts
index fb0f2b5..cc92958 100644
--- a/frontend/src/api/pendingLinkages.ts
+++ b/frontend/src/api/pendingLinkages.ts
@@ -15,6 +15,7 @@ export interface PendingLinkageResponse {
updated_at: string | null
photo_filename: string | null
photo_path: string | null
+ photo_media_type: string | null
photo_tags: string[]
}
diff --git a/frontend/src/api/reportedPhotos.ts b/frontend/src/api/reportedPhotos.ts
index 07ae8ef..1f03bc7 100644
--- a/frontend/src/api/reportedPhotos.ts
+++ b/frontend/src/api/reportedPhotos.ts
@@ -14,6 +14,7 @@ export interface ReportedPhotoResponse {
report_comment: string | null
photo_path: string | null
photo_filename: string | null
+ photo_media_type: string | null
}
export interface ReportedPhotosListResponse {
diff --git a/frontend/src/pages/PendingPhotos.tsx b/frontend/src/pages/PendingPhotos.tsx
index 04cc055..c32175f 100644
--- a/frontend/src/pages/PendingPhotos.tsx
+++ b/frontend/src/pages/PendingPhotos.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef, useMemo } from 'react'
import { pendingPhotosApi, PendingPhotoResponse, ReviewDecision, CleanupResponse } from '../api/pendingPhotos'
import { apiClient } from '../api/client'
import { useAuth } from '../context/AuthContext'
+import { videosApi } from '../api/videos'
type SortKey = 'photo' | 'uploaded_by' | 'file_info' | 'submitted_at' | 'status'
@@ -625,22 +626,42 @@ export default function PendingPhotos() {
{
- // For full-size view, fetch as blob and open in new tab
- try {
- const blobUrl = imageUrls[photo.id] || await pendingPhotosApi.getPendingPhotoImageBlob(photo.id)
- // Create a new window with the blob URL
- const newWindow = window.open()
- if (newWindow) {
- newWindow.location.href = blobUrl
+ const isVideo = photo.mime_type?.startsWith('video/')
+ if (isVideo) {
+ // For videos, open the video file directly
+ const videoUrl = `${apiClient.defaults.baseURL}/api/v1/pending-photos/${photo.id}/image`
+ window.open(videoUrl, '_blank')
+ } else {
+ // For images, fetch as blob and open in new tab
+ try {
+ const blobUrl = imageUrls[photo.id] || await pendingPhotosApi.getPendingPhotoImageBlob(photo.id)
+ // Create a new window with the blob URL
+ const newWindow = window.open()
+ if (newWindow) {
+ newWindow.location.href = blobUrl
+ }
+ } catch (err) {
+ console.error('Failed to open full-size image:', err)
+ alert('Failed to load full-size image')
}
- } catch (err) {
- console.error('Failed to open full-size image:', err)
- alert('Failed to load full-size image')
}
}}
- title="Click to open full photo"
+ title={photo.mime_type?.startsWith('video/') ? 'Click to open video' : 'Click to open full photo'}
>
- {imageUrls[photo.id] ? (
+ {photo.mime_type?.startsWith('video/') ? (
+
+ ) : imageUrls[photo.id] ? (

{
- const photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${reported.photo_id}/image`
- window.open(photoUrl, '_blank')
+ const isVideo = reported.photo_media_type === 'video'
+ const url = isVideo
+ ? videosApi.getVideoUrl(reported.photo_id)
+ : `${apiClient.defaults.baseURL}/api/v1/photos/${reported.photo_id}/image`
+ window.open(url, '_blank')
}}
- title="Click to open full photo"
+ title={reported.photo_media_type === 'video' ? 'Click to open video' : 'Click to open full photo'}
>
-

{
- const target = e.target as HTMLImageElement
- target.style.display = 'none'
- const parent = target.parentElement
- if (parent && !parent.querySelector('.error-fallback')) {
- const fallback = document.createElement('div')
- fallback.className =
- 'text-gray-400 text-xs error-fallback'
- fallback.textContent = `#${reported.photo_id}`
- parent.appendChild(fallback)
- }
- }}
- />
+ {reported.photo_media_type === 'video' ? (
+
})
{
+ const target = e.target as HTMLImageElement
+ target.style.display = 'none'
+ const parent = target.parentElement
+ if (parent && !parent.querySelector('.error-fallback')) {
+ const fallback = document.createElement('div')
+ fallback.className =
+ 'text-gray-400 text-xs error-fallback'
+ fallback.textContent = `#${reported.photo_id}`
+ parent.appendChild(fallback)
+ }
+ }}
+ />
+ ) : (
+

{
+ const target = e.target as HTMLImageElement
+ target.style.display = 'none'
+ const parent = target.parentElement
+ if (parent && !parent.querySelector('.error-fallback')) {
+ const fallback = document.createElement('div')
+ fallback.className =
+ 'text-gray-400 text-xs error-fallback'
+ fallback.textContent = `#${reported.photo_id}`
+ parent.appendChild(fallback)
+ }
+ }}
+ />
+ )}
) : (
Photo not found
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx
index 132acfc..c321bab 100644
--- a/frontend/src/pages/Search.tsx
+++ b/frontend/src/pages/Search.tsx
@@ -93,8 +93,11 @@ export default function Search() {
page_size: pageSize,
}
- // Add media type filter if not 'all'
- if (mediaType && mediaType !== 'all') {
+ // For "Photos without faces" search, always exclude videos
+ if (searchType === 'no_faces') {
+ params.media_type = 'image'
+ } else if (mediaType && mediaType !== 'all') {
+ // Add media type filter if not 'all' for other search types
params.media_type = mediaType
}
diff --git a/frontend/src/pages/UserTaggedPhotos.tsx b/frontend/src/pages/UserTaggedPhotos.tsx
index 9aa6272..0dd34ca 100644
--- a/frontend/src/pages/UserTaggedPhotos.tsx
+++ b/frontend/src/pages/UserTaggedPhotos.tsx
@@ -6,6 +6,7 @@ import {
} from '../api/pendingLinkages'
import { apiClient } from '../api/client'
import { useAuth } from '../context/AuthContext'
+import { videosApi } from '../api/videos'
type DecisionValue = 'approve' | 'deny'
@@ -440,28 +441,51 @@ export default function UserTaggedPhotos() {
{
- const url = `${apiClient.defaults.baseURL}/api/v1/photos/${linkage.photo_id}/image`
+ const isVideo = linkage.photo_media_type === 'video'
+ const url = isVideo
+ ? videosApi.getVideoUrl(linkage.photo_id)
+ : `${apiClient.defaults.baseURL}/api/v1/photos/${linkage.photo_id}/image`
window.open(url, '_blank')
}}
- title="Open photo in new tab"
+ title={linkage.photo_media_type === 'video' ? 'Open video in new tab' : 'Open photo in new tab'}
>
-

{
- const target = event.target as HTMLImageElement
- target.style.display = 'none'
- const parent = target.parentElement
- if (parent && !parent.querySelector('.fallback-text')) {
- const fallback = document.createElement('div')
- fallback.className = 'fallback-text text-gray-400 text-xs text-center'
- fallback.textContent = `#${linkage.photo_id}`
- parent.appendChild(fallback)
- }
- }}
- />
+ {linkage.photo_media_type === 'video' ? (
+
})
{
+ const target = event.target as HTMLImageElement
+ target.style.display = 'none'
+ const parent = target.parentElement
+ if (parent && !parent.querySelector('.fallback-text')) {
+ const fallback = document.createElement('div')
+ fallback.className = 'fallback-text text-gray-400 text-xs text-center'
+ fallback.textContent = `#${linkage.photo_id}`
+ parent.appendChild(fallback)
+ }
+ }}
+ />
+ ) : (
+

{
+ const target = event.target as HTMLImageElement
+ target.style.display = 'none'
+ const parent = target.parentElement
+ if (parent && !parent.querySelector('.fallback-text')) {
+ const fallback = document.createElement('div')
+ fallback.className = 'fallback-text text-gray-400 text-xs text-center'
+ fallback.textContent = `#${linkage.photo_id}`
+ parent.appendChild(fallback)
+ }
+ }}
+ />
+ )}
) : (
Photo not found
diff --git a/src/web/api/pending_linkages.py b/src/web/api/pending_linkages.py
index bb99492..73e9d07 100644
--- a/src/web/api/pending_linkages.py
+++ b/src/web/api/pending_linkages.py
@@ -67,6 +67,7 @@ class PendingLinkageResponse(BaseModel):
updated_at: Optional[str] = None
photo_filename: Optional[str] = None
photo_path: Optional[str] = None
+ photo_media_type: Optional[str] = None
photo_tags: list[str] = Field(default_factory=list)
@@ -221,6 +222,7 @@ def list_pending_linkages(
updated_at=updated_at,
photo_filename=photo.filename if photo else None,
photo_path=photo.path if photo else None,
+ photo_media_type=photo.media_type if photo else None,
photo_tags=photo_tags_map.get(row.photo_id, []),
)
)
diff --git a/src/web/api/reported_photos.py b/src/web/api/reported_photos.py
index 8fd31a7..e54108d 100644
--- a/src/web/api/reported_photos.py
+++ b/src/web/api/reported_photos.py
@@ -36,6 +36,7 @@ class ReportedPhotoResponse(BaseModel):
# Photo details from main database
photo_path: Optional[str] = None
photo_filename: Optional[str] = None
+ photo_media_type: Optional[str] = None
class ReportedPhotosListResponse(BaseModel):
@@ -147,10 +148,12 @@ def list_reported_photos(
# Get photo details from main database
photo_path = None
photo_filename = None
+ photo_media_type = None
photo = main_db.query(Photo).filter(Photo.id == row.photo_id).first()
if photo:
photo_path = photo.path
photo_filename = photo.filename
+ photo_media_type = photo.media_type
items.append(ReportedPhotoResponse(
id=row.id,
@@ -166,6 +169,7 @@ def list_reported_photos(
report_comment=row.report_comment,
photo_path=photo_path,
photo_filename=photo_filename,
+ photo_media_type=photo_media_type,
))
return ReportedPhotosListResponse(items=items, total=len(items))