From 0c212348f6ddf024aab74cd35ac19a5cc69f123d Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Tue, 2 Dec 2025 16:19:02 -0500 Subject: [PATCH] feat: Add photo_media_type field to API responses and enhance media handling in frontend This commit introduces a new `photo_media_type` field in the `PendingLinkageResponse` and `ReportedPhotoResponse` interfaces, allowing differentiation between image and video files. The frontend has been updated to handle video links appropriately, including opening video files directly and displaying video thumbnails. Additionally, the search functionality has been enhanced to exclude videos when searching for "Photos without faces." Documentation has been updated to reflect these changes. --- frontend/src/api/pendingLinkages.ts | 1 + frontend/src/api/reportedPhotos.ts | 1 + frontend/src/pages/PendingPhotos.tsx | 45 ++++++++++++----- frontend/src/pages/ReportedPhotos.tsx | 67 +++++++++++++++++-------- frontend/src/pages/Search.tsx | 7 ++- frontend/src/pages/UserTaggedPhotos.tsx | 62 ++++++++++++++++------- src/web/api/pending_linkages.py | 2 + src/web/api/reported_photos.py | 4 ++ 8 files changed, 135 insertions(+), 54 deletions(-) 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/') ? ( +
+ + + +
+ VIDEO +
+
+ ) : imageUrls[photo.id] ? ( {photo.original_filename} { - 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'} > - {`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' ? ( + {`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) + } + }} + /> + ) : ( + {`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) + } + }} + /> + )}
) : (
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'} > - {`Photo { - 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' ? ( + {`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) + } + }} + /> + ) : ( + {`Photo { + 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))