Comprehensive feature plan for filtering photos by guest feedback (ratings, likes, favorites) and exporting selections for professional photo editing workflows. Export formats supported: - TXT: Simple filename list for Lightroom filter paste - CSV: Spreadsheet with metadata columns - XMP: Sidecar files with ratings/labels for Lightroom/Capture One - ZIP: Original photos with folder organization - JSON: Structured metadata for automation Key features: - Admin filter UI with rating thresholds and feedback toggles - AND/OR filter logic - Quick presets (Guest Picks, Top Rated, Most Popular) - Photo selection with batch actions - XMP rating mapping (PicPeak 1-5 → XMP 1-5 + color labels) - Background job support for large exports - Export settings dialog with customization options Research references: - Adobe XMP/Lightroom metadata standards - Capture One EIP format - IPTC Photo Metadata Standard - ExifTool capabilities
27 KiB
Feature: Photo Filtering & Export for Professional Workflows
Overview
This feature enables administrators to filter photos by guest feedback (rated, liked, favorited) and export filtered selections in formats compatible with professional photo editing tools like Adobe Lightroom, Capture One, Photo Mechanic, and Affinity Photo.
Priority: Low Status: Open
Problem Statement
Currently, PicPeak collects valuable guest feedback (ratings, likes, favorites) but administrators cannot easily:
- Filter photos in the admin panel based on this feedback
- Export filtered selections for use in professional photo editing workflows
- Identify "client picks" to prioritize in post-processing
Professional photographers need to:
- Quickly identify which photos guests loved most
- Export these selections to Lightroom/Capture One for final editing
- Create client-approved photo sets without manual searching
User Stories
-
As an administrator, I want to filter photos by rating, likes, or favorites, so that I can quickly see which photos guests preferred.
-
As an administrator, I want to export a filtered selection of photos, so that I can import them into Lightroom or Capture One for final processing.
-
As an administrator, I want multiple export format options, so that I can use whichever format works best with my photo editing software.
Research: Professional Photo Tool Import Formats
Industry Standards
Based on research into professional photo editing workflows:
| Format | Description | Compatibility |
|---|---|---|
| XMP Sidecar | XML-based metadata files with ratings/labels | Lightroom, Bridge, Capture One, ACR |
| CSV/Text List | Simple filename list for manual selection | All tools (via search/filter) |
| ZIP Archive | Direct photo export with folder structure | All tools (file import) |
| JSON Metadata | Structured data for programmatic workflows | Scripts, automation |
Sources Referenced
- Adobe XMP Metadata Documentation
- Capture One EIP Format
- IPTC Photo Metadata Standard
- ExifTool Documentation
- Lightroom Filename Search Method
Current PicPeak Feedback System
Existing Features (from codebase analysis)
| Feedback Type | Database Column | Scale | Guest Toggle |
|---|---|---|---|
| Ratings | average_rating, feedback_count |
1-5 stars | Update/Remove |
| Likes | like_count |
Count | Toggle on/off |
| Favorites | favorite_count |
Count | Toggle on/off |
| Comments | photo_feedback table |
Text | No |
Database Schema (photos table)
average_rating DECIMAL(3,2) -- 0.00 to 5.00
feedback_count INTEGER -- Total rating count
like_count INTEGER -- Total likes
favorite_count INTEGER -- Total favorites
Existing Export (feedback only)
- Endpoint:
GET /admin/feedback/events/:eventId/feedback/export - Formats: JSON, CSV
- Exports feedback records, not filtered photo lists
Feature Requirements
1. Admin Photo Filter UI
1.1 Filter Panel Location
Path: Admin Panel → Event → Photos Tab
Add filter controls above the photo grid:
┌─────────────────────────────────────────────────────────────────────┐
│ Photos [Upload] │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─ Filters ──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Feedback: [All ▼] [Rated ▼] [≥ 3 stars ▼] │ │
│ │ │ │
│ │ ☑ Has likes (15) ☑ Has favorites (8) ☐ Has comments (3) │ │
│ │ │ │
│ │ [Clear Filters] Showing: 23 of 150 photos │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ [Select All Filtered] [Export Selection ▼] │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ ☑ │ │ ☑ │ │ ☑ │ │ ☐ │ │ ☐ │ │ ☐ │ ... │
│ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │ IMG │ │
│ │ ⭐⭐⭐│ │ ⭐⭐⭐│ │ ⭐⭐⭐│ │ │ │ │ │ │ │
│ │ ❤ 5 │ │ ❤ 3 │ │ ❤ 8 │ │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────────────────────┘
1.2 Filter Options
| Filter | Type | Options |
|---|---|---|
| Rating | Dropdown | All, Any Rating, ≥1 star, ≥2 stars, ≥3 stars, ≥4 stars, 5 stars only |
| Likes | Checkbox | Has likes (show count) |
| Favorites | Checkbox | Has favorites (show count) |
| Comments | Checkbox | Has comments (show count) |
| Combine Logic | Toggle | AND / OR (default: AND) |
1.3 Quick Filter Presets
| Preset | Description | Filter Applied |
|---|---|---|
| "Guest Picks" | Photos guests loved | Rating ≥4 OR Likes ≥3 OR Favorites ≥1 |
| "Top Rated" | Highest rated photos | Rating ≥4 stars |
| "Most Popular" | Most liked photos | Sorted by like_count DESC |
| "Client Favorites" | Favorited by guests | Has favorites |
2. Photo Selection & Batch Actions
2.1 Selection Controls
- Individual photo checkbox selection
- "Select All Filtered" button (selects all photos matching current filter)
- "Select All" button (selects all photos regardless of filter)
- "Deselect All" button
- Selection counter: "23 photos selected"
2.2 Selection Persistence
- Selection persists when changing filters
- Selection cleared on page navigation (with confirmation if >0 selected)
3. Export Functionality
3.1 Export Button/Menu
┌──────────────────────────────┐
│ Export Selection ▼ │
├──────────────────────────────┤
│ 📋 Filename List (TXT) │ ← Simple text list
│ 📊 Filename List (CSV) │ ← Spreadsheet compatible
│ 📦 XMP Sidecar Files (ZIP) │ ← Lightroom/Bridge/C1
│ 🗂️ Original Photos (ZIP) │ ← Direct file export
│ 📄 Metadata (JSON) │ ← Programmatic use
│ ──────────────────────────── │
│ ⚙️ Export Settings... │ ← Configure defaults
└──────────────────────────────┘
3.2 Export Formats
Format 1: Filename List (TXT)
Use Case: Paste into Lightroom's Library Filter to select matching photos
Output:
IMG_0001.jpg
IMG_0015.jpg
IMG_0023.jpg
IMG_0089.jpg
Lightroom Workflow:
- Export TXT from PicPeak
- In Lightroom: Library → Filter → Text → Filename → Contains
- Paste comma-separated list:
IMG_0001, IMG_0015, IMG_0023, IMG_0089 - Select filtered photos → Add to Collection
Format 2: Filename List (CSV)
Use Case: Spreadsheet analysis, Photo Mechanic code replacement
Output:
filename,original_filename,rating,likes,favorites,category
Test_Wedding_individual_0001.jpg,IMG_0001.jpg,4.5,12,3,Ceremony
Test_Wedding_individual_0015.jpg,IMG_0015.jpg,5.0,8,5,Reception
Test_Wedding_individual_0023.jpg,IMG_0023.jpg,4.0,15,2,Portraits
Format 3: XMP Sidecar Files (ZIP)
Use Case: Import ratings/labels directly into Lightroom, Bridge, Capture One
Output Structure:
export_wedding_picks_2026-01-01.zip
├── IMG_0001.xmp
├── IMG_0015.xmp
├── IMG_0023.xmp
└── IMG_0089.xmp
XMP Content Example:
<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
xmp:Rating="4"
xmp:Label="Yellow"
photoshop:Instructions="Guest Rating: 4.5 stars, 12 likes, 3 favorites">
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">PicPeak Export - Guest Picks</rdf:li>
</rdf:Alt>
</dc:description>
<Iptc4xmpCore:SubjectCode>
<rdf:Bag>
<rdf:li>guest-pick</rdf:li>
<rdf:li>rating-4</rdf:li>
</rdf:Bag>
</Iptc4xmpCore:SubjectCode>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
XMP Rating Mapping:
| PicPeak Rating | XMP Rating | XMP Label |
|---|---|---|
| 4.5 - 5.0 | 5 | Red |
| 3.5 - 4.4 | 4 | Yellow |
| 2.5 - 3.4 | 3 | Green |
| 1.5 - 2.4 | 2 | Blue |
| 0.5 - 1.4 | 1 | Purple |
| No rating | 0 | None |
Lightroom Import Workflow:
- Export XMP ZIP from PicPeak
- Extract to folder containing original RAW/JPEG files
- In Lightroom: Select photos → Metadata → Read Metadata from Files
- Ratings and labels appear on photos
Format 4: Original Photos (ZIP)
Use Case: Direct photo export for clients or backup
Output Structure:
export_wedding_picks_2026-01-01.zip
├── Ceremony/
│ ├── IMG_0001.jpg
│ └── IMG_0002.jpg
├── Reception/
│ ├── IMG_0015.jpg
│ └── IMG_0016.jpg
└── export_manifest.json
Options:
- Include/exclude categories subfolder organization
- Include/exclude thumbnails
- Include/exclude metadata JSON
- Quality: Original / High (2048px) / Medium (1024px)
Format 5: Metadata JSON
Use Case: Programmatic workflows, custom integrations
Output:
{
"export_info": {
"event_name": "Test Wedding Gallery",
"event_date": "2026-01-01",
"exported_at": "2026-01-02T10:30:00Z",
"filter_applied": {
"min_rating": 4,
"has_likes": true,
"has_favorites": null
},
"total_photos": 23
},
"photos": [
{
"filename": "Test_Wedding_individual_0001.jpg",
"original_filename": "IMG_0001.jpg",
"path": "wedding-test-2026-01-01/individual/Test_Wedding_individual_0001.jpg",
"category": "Ceremony",
"rating": {
"average": 4.5,
"count": 8
},
"likes": 12,
"favorites": 3,
"comments": 2,
"dimensions": {
"width": 5472,
"height": 3648
},
"size_bytes": 4521984,
"uploaded_at": "2026-01-01T14:30:00Z"
}
]
}
4. Export Settings Dialog
┌─────────────────────────────────────────────────────────────────┐
│ Export Settings [X] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ XMP Sidecar Options │
│ ─────────────────────────────────────────────────────────────── │
│ Filename matching: ○ PicPeak filename (Test_Wedding_0001.jpg) │
│ ● Original filename (IMG_0001.jpg) │
│ │
│ Include in XMP: ☑ Rating (as XMP Rating 1-5) │
│ ☑ Color label (based on rating) │
│ ☑ Description (feedback summary) │
│ ☑ Keywords (guest-pick, category) │
│ │
│ ─────────────────────────────────────────────────────────────── │
│ Photo Export Options │
│ ─────────────────────────────────────────────────────────────── │
│ Quality: ○ Original ● High (2048px) ○ Medium │
│ Organization: ☑ Group by category │
│ Include: ☑ Metadata JSON ☐ Thumbnails │
│ │
│ ─────────────────────────────────────────────────────────────── │
│ CSV/TXT Options │
│ ─────────────────────────────────────────────────────────────── │
│ Filename format: ○ PicPeak filename ● Original filename │
│ Separator: ○ Newline ● Comma ○ Semicolon │
│ │
│ [Cancel] [Save as Default] [OK] │
└─────────────────────────────────────────────────────────────────┘
Technical Specification
5. API Endpoints
5.1 Get Filtered Photos
GET /api/admin/photos/:eventId/filtered
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
min_rating |
number | Minimum average rating (0-5) |
max_rating |
number | Maximum average rating (0-5) |
has_likes |
boolean | Filter photos with likes |
min_likes |
number | Minimum like count |
has_favorites |
boolean | Filter photos with favorites |
min_favorites |
number | Minimum favorite count |
has_comments |
boolean | Filter photos with comments |
category_id |
number | Filter by category |
logic |
string | 'AND' or 'OR' (default: AND) |
sort |
string | 'rating', 'likes', 'favorites', 'date' |
order |
string | 'asc' or 'desc' |
page |
number | Page number |
limit |
number | Photos per page |
Response:
{
"success": true,
"data": {
"photos": [...],
"pagination": {
"total": 150,
"filtered": 23,
"page": 1,
"limit": 50,
"pages": 1
},
"summary": {
"with_ratings": 45,
"with_likes": 78,
"with_favorites": 23,
"with_comments": 12
}
}
}
5.2 Export Photos
POST /api/admin/photos/:eventId/export
Request Body:
{
"photo_ids": [1, 5, 12, 23, 45],
"format": "xmp",
"options": {
"filename_format": "original",
"include_rating": true,
"include_label": true,
"include_description": true,
"include_keywords": true
}
}
Or with filter (export all matching):
{
"filter": {
"min_rating": 4,
"has_likes": true
},
"format": "xmp",
"options": {...}
}
Response:
- For small exports: Direct file download
- For large exports: Background job with status polling
{
"success": true,
"job_id": "export_abc123",
"status": "processing",
"progress": 45,
"estimated_time": 30
}
5.3 Check Export Status
GET /api/admin/exports/:jobId/status
5.4 Download Export
GET /api/admin/exports/:jobId/download
6. Backend Implementation
6.1 New Files
| File | Purpose |
|---|---|
/backend/src/routes/adminPhotoExport.js |
Export API routes |
/backend/src/services/photoExportService.js |
Export logic |
/backend/src/services/xmpGenerator.js |
XMP file generation |
/backend/src/utils/photoFilterBuilder.js |
Query builder for filters |
6.2 XMP Generator Service
// /backend/src/services/xmpGenerator.js
class XmpGenerator {
/**
* Generate XMP sidecar content for a photo
*/
generateXmp(photo, options = {}) {
const rating = this.mapRating(photo.average_rating);
const label = this.mapLabel(photo.average_rating);
return `<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
xmp:Rating="${rating}"
${label ? `xmp:Label="${label}"` : ''}>
${this.generateDescription(photo, options)}
${this.generateKeywords(photo, options)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>`;
}
mapRating(avgRating) {
if (!avgRating || avgRating === 0) return 0;
if (avgRating >= 4.5) return 5;
if (avgRating >= 3.5) return 4;
if (avgRating >= 2.5) return 3;
if (avgRating >= 1.5) return 2;
return 1;
}
mapLabel(avgRating) {
if (!avgRating || avgRating === 0) return null;
if (avgRating >= 4.5) return 'Red'; // Top picks
if (avgRating >= 3.5) return 'Yellow'; // Good
if (avgRating >= 2.5) return 'Green'; // Average
if (avgRating >= 1.5) return 'Blue'; // Below average
return 'Purple'; // Low
}
generateDescription(photo, options) {
if (!options.include_description) return '';
const desc = `PicPeak Guest Feedback: ${photo.average_rating?.toFixed(1) || 0} stars, ${photo.like_count || 0} likes, ${photo.favorite_count || 0} favorites`;
return `<dc:description><rdf:Alt><rdf:li xml:lang="x-default">${desc}</rdf:li></rdf:Alt></dc:description>`;
}
generateKeywords(photo, options) {
if (!options.include_keywords) return '';
const keywords = ['picpeak-export'];
if (photo.average_rating >= 4) keywords.push('guest-pick');
if (photo.category_name) keywords.push(photo.category_name.toLowerCase());
return `<dc:subject><rdf:Bag>${keywords.map(k => `<rdf:li>${k}</rdf:li>`).join('')}</rdf:Bag></dc:subject>`;
}
}
6.3 Export Service
// /backend/src/services/photoExportService.js
class PhotoExportService {
async exportPhotos(eventId, photoIds, format, options) {
const photos = await this.getPhotosWithFeedback(eventId, photoIds);
switch (format) {
case 'txt':
return this.exportAsTxt(photos, options);
case 'csv':
return this.exportAsCsv(photos, options);
case 'xmp':
return this.exportAsXmpZip(photos, options);
case 'photos':
return this.exportPhotosZip(photos, options);
case 'json':
return this.exportAsJson(photos, options);
default:
throw new Error(`Unknown format: ${format}`);
}
}
async exportAsXmpZip(photos, options) {
const archive = archiver('zip', { zlib: { level: 9 } });
const xmpGenerator = new XmpGenerator();
for (const photo of photos) {
const filename = options.filename_format === 'original'
? photo.original_filename
: photo.filename;
const xmpFilename = filename.replace(/\.[^.]+$/, '.xmp');
const xmpContent = xmpGenerator.generateXmp(photo, options);
archive.append(xmpContent, { name: xmpFilename });
}
return archive;
}
// ... other export methods
}
7. Frontend Implementation
7.1 New Components
| Component | Purpose |
|---|---|
PhotoFilterPanel.tsx |
Filter controls UI |
PhotoExportMenu.tsx |
Export dropdown menu |
ExportSettingsDialog.tsx |
Export configuration modal |
ExportProgressModal.tsx |
Progress indicator for large exports |
7.2 Filter Panel Component
// /frontend/src/components/admin/PhotoFilterPanel.tsx
interface PhotoFilters {
minRating: number | null;
hasLikes: boolean;
hasFavorites: boolean;
hasComments: boolean;
logic: 'AND' | 'OR';
}
interface PhotoFilterPanelProps {
filters: PhotoFilters;
onChange: (filters: PhotoFilters) => void;
summary: {
total: number;
filtered: number;
withRatings: number;
withLikes: number;
withFavorites: number;
};
}
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters,
onChange,
summary
}) => {
// Component implementation
};
7.3 State Management
// Use React Query for filtered photos
const useFilteredPhotos = (eventId: string, filters: PhotoFilters) => {
return useQuery({
queryKey: ['photos', eventId, 'filtered', filters],
queryFn: () => photoService.getFilteredPhotos(eventId, filters),
keepPreviousData: true
});
};
// Selection state
const [selectedPhotoIds, setSelectedPhotoIds] = useState<Set<number>>(new Set());
8. Database Considerations
8.1 Indexing for Filter Performance
-- Add indexes for common filter queries
CREATE INDEX idx_photos_average_rating ON photos(event_id, average_rating) WHERE average_rating > 0;
CREATE INDEX idx_photos_like_count ON photos(event_id, like_count) WHERE like_count > 0;
CREATE INDEX idx_photos_favorite_count ON photos(event_id, favorite_count) WHERE favorite_count > 0;
8.2 Export Jobs Table (for large exports)
CREATE TABLE export_jobs (
id SERIAL PRIMARY KEY,
job_id VARCHAR(50) UNIQUE NOT NULL,
event_id INTEGER REFERENCES events(id),
admin_user_id INTEGER REFERENCES admin_users(id),
format VARCHAR(20) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
progress INTEGER DEFAULT 0,
total_photos INTEGER,
options JSONB,
file_path VARCHAR(500),
file_size BIGINT,
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP
);
Implementation Checklist
Phase 1: Backend - Filtering
- Create
photoFilterBuilder.jsutility - Add filtered photos endpoint to admin routes
- Add database indexes for performance
- Write unit tests for filter logic
Phase 2: Backend - Export
- Create
xmpGenerator.jsservice - Create
photoExportService.jsservice - Add export endpoint to admin routes
- Implement TXT export
- Implement CSV export
- Implement XMP ZIP export
- Implement Photos ZIP export
- Implement JSON export
- Add background job support for large exports
- Write unit tests for export formats
Phase 3: Frontend - Filter UI
- Create
PhotoFilterPanel.tsxcomponent - Integrate filter panel into Photos tab
- Add filter state management
- Implement filter summary display
- Add preset filter buttons
Phase 4: Frontend - Selection
- Add photo selection checkboxes
- Implement "Select All Filtered" functionality
- Add selection counter
- Persist selection across filter changes
Phase 5: Frontend - Export
- Create
PhotoExportMenu.tsxcomponent - Create
ExportSettingsDialog.tsxcomponent - Create
ExportProgressModal.tsxcomponent - Integrate export functionality
- Handle download responses
Phase 6: Testing & Documentation
- End-to-end testing with real Lightroom import
- Test Capture One XMP compatibility
- Test with large photo sets (500+ photos)
- Document workflow guides for users
- Update API documentation
Test Scenarios
| # | Scenario | Expected Result |
|---|---|---|
| 1 | Filter by rating ≥4 stars | Only high-rated photos shown |
| 2 | Filter by likes AND favorites | Photos with both shown |
| 3 | Filter by likes OR favorites | Photos with either shown |
| 4 | Export XMP for 10 photos | ZIP with 10 .xmp files |
| 5 | Import XMP into Lightroom | Ratings appear on matched photos |
| 6 | Export TXT list | Newline-separated filename list |
| 7 | Export large set (500 photos) | Background job with progress |
Acceptance Criteria
- ✅ Admin can filter photos by rating threshold
- ✅ Admin can filter photos by likes/favorites/comments
- ✅ Admin can combine filters with AND/OR logic
- ✅ Filter counts shown in UI
- ✅ Admin can select individual or all filtered photos
- ✅ Export menu shows all format options
- ✅ TXT export contains filename list
- ✅ CSV export contains metadata columns
- ✅ XMP export creates valid sidecar files
- ✅ XMP files import correctly into Lightroom
- ✅ ZIP export includes organized photos
- ✅ Large exports show progress indicator
- ✅ Export settings can be customized
Security Considerations
- Admin authentication required for all endpoints
- Event ownership validation before export
- Rate limiting on export endpoints
- Temporary file cleanup after download
- No sensitive data in exported files
Related Files
Backend
/backend/src/routes/adminPhotos.js- Existing photo routes/backend/src/routes/adminFeedback.js- Existing feedback export/backend/src/services/feedbackService.js- Feedback queries
Frontend
/frontend/src/components/admin/EventPhotos.tsx- Photos tab/frontend/src/components/admin/PhotoGrid.tsx- Photo display/frontend/src/services/photoService.ts- Photo API service
Future Enhancements
- Scheduled exports: Auto-export daily/weekly
- Email delivery: Send export to admin email
- Cloud storage: Export directly to Dropbox/Google Drive
- Custom XMP templates: User-defined XMP structures
- Batch operations: Apply ratings/labels in bulk from PicPeak
- Two-way sync: Import ratings back from Lightroom