Skip to content

Commit 07094d5

Browse files
committed
image batch operation
1 parent c37d8df commit 07094d5

21 files changed

Lines changed: 1036 additions & 26 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ Handles file system operations and provides a secure bridge between the frontend
3232

3333
## Features
3434

35-
- Smart tagging of photos based on detected objects, faces, and their recognition
36-
- Traditional gallery features of album management
37-
- Advanced image analysis with object detection and facial recognition
38-
- Privacy-focused design with offline functionality
39-
- Efficient data handling and parallel processing
40-
- Smart search and retrieval
41-
- Cross-platform compatibility
35+
- **Smart Tagging**: Automatic photo tagging based on detected objects, faces, and their recognition
36+
- **Batch Operations**: Select multiple images and perform bulk actions (delete, tag, move to album, export)
37+
- **Album Management**: Traditional gallery features for organizing your photos
38+
- **Advanced Image Analysis**: Object detection and facial recognition powered by AI
39+
- **Privacy-Focused**: Offline functionality with all processing done locally
40+
- **Efficient Processing**: Parallel processing for handling large photo collections
41+
- **Smart Search**: Quick retrieval with advanced search capabilities
42+
- **Cross-Platform**: Works on Windows, macOS, and Linux
43+
- **Keyboard Shortcuts**: Ctrl+A to toggle select/deselect all, Escape to deselect (works on all platforms including Mac)
4244

4345
## Technical Stack
4446

backend/app/database/images.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,97 @@ def db_toggle_image_favourite_status(image_id: str) -> bool:
419419
return False
420420
finally:
421421
conn.close()
422+
423+
424+
425+
def db_delete_image(image_id: ImageId) -> bool:
426+
"""
427+
Delete a single image from the database by its ID.
428+
This will also delete associated records in image_classes due to CASCADE.
429+
430+
Args:
431+
image_id: ID of the image to delete
432+
433+
Returns:
434+
True if deletion was successful, False otherwise
435+
"""
436+
conn = _connect()
437+
cursor = conn.cursor()
438+
439+
try:
440+
cursor.execute("DELETE FROM images WHERE id = ?", (image_id,))
441+
conn.commit()
442+
return cursor.rowcount > 0
443+
except Exception as e:
444+
logger.error(f"Error deleting image {image_id}: {e}")
445+
conn.rollback()
446+
return False
447+
finally:
448+
conn.close()
449+
450+
451+
def db_add_tags_to_image(image_id: ImageId, tags: List[str]) -> bool:
452+
"""
453+
Add multiple tags to an image.
454+
455+
Args:
456+
image_id: ID of the image to tag
457+
tags: List of tag names to add
458+
459+
Returns:
460+
True if tagging was successful, False otherwise
461+
"""
462+
if not tags:
463+
return True
464+
465+
conn = _connect()
466+
cursor = conn.cursor()
467+
468+
try:
469+
# First, verify the image exists
470+
cursor.execute("SELECT id FROM images WHERE id = ?", (image_id,))
471+
if not cursor.fetchone():
472+
logger.warning(f"Image {image_id} not found")
473+
return False
474+
475+
# Get or create class IDs for each tag
476+
class_ids = []
477+
for tag in tags:
478+
# Check if tag exists in mappings
479+
cursor.execute("SELECT class_id FROM mappings WHERE name = ?", (tag,))
480+
result = cursor.fetchone()
481+
482+
if result:
483+
class_ids.append(result[0])
484+
else:
485+
# Create new mapping for this tag
486+
cursor.execute(
487+
"INSERT INTO mappings (name) VALUES (?)",
488+
(tag,)
489+
)
490+
class_ids.append(cursor.lastrowid)
491+
492+
# Insert image-class pairs
493+
for class_id in class_ids:
494+
cursor.execute(
495+
"""
496+
INSERT OR IGNORE INTO image_classes (image_id, class_id)
497+
VALUES (?, ?)
498+
""",
499+
(image_id, class_id),
500+
)
501+
502+
# Mark image as tagged
503+
cursor.execute(
504+
"UPDATE images SET isTagged = 1 WHERE id = ?",
505+
(image_id,)
506+
)
507+
508+
conn.commit()
509+
return True
510+
except Exception as e:
511+
logger.error(f"Error adding tags to image {image_id}: {e}")
512+
conn.rollback()
513+
return False
514+
finally:
515+
conn.close()

backend/app/routes/images.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,95 @@ class ImageInfoResponse(BaseModel):
128128
isTagged: bool
129129
isFavourite: bool
130130
tags: Optional[List[str]] = None
131+
132+
133+
# Batch Operations
134+
from app.schemas.batch import (
135+
BatchDeleteRequest,
136+
BatchTagRequest,
137+
BatchMoveRequest,
138+
BatchOperationResponse
139+
)
140+
from app.database.images import db_delete_image, db_add_tags_to_image
141+
142+
143+
@router.delete("/batch-delete", response_model=BatchOperationResponse)
144+
def batch_delete_images(request: BatchDeleteRequest):
145+
"""Delete multiple images"""
146+
processed = 0
147+
failed = 0
148+
errors = []
149+
150+
for image_id in request.image_ids:
151+
try:
152+
success = db_delete_image(image_id)
153+
if success:
154+
processed += 1
155+
else:
156+
failed += 1
157+
errors.append(f"Image {image_id} not found")
158+
except Exception as e:
159+
failed += 1
160+
errors.append(f"Failed to delete {image_id}: {str(e)}")
161+
logger.error(f"Error deleting image {image_id}: {e}")
162+
163+
return BatchOperationResponse(
164+
success=failed == 0,
165+
processed=processed,
166+
failed=failed,
167+
errors=errors
168+
)
169+
170+
171+
@router.post("/batch-tag", response_model=BatchOperationResponse)
172+
def batch_tag_images(request: BatchTagRequest):
173+
"""Add tags to multiple images"""
174+
processed = 0
175+
failed = 0
176+
errors = []
177+
178+
for image_id in request.image_ids:
179+
try:
180+
success = db_add_tags_to_image(image_id, request.tags)
181+
if success:
182+
processed += 1
183+
else:
184+
failed += 1
185+
errors.append(f"Image {image_id} not found")
186+
except Exception as e:
187+
failed += 1
188+
errors.append(f"Failed to tag {image_id}: {str(e)}")
189+
logger.error(f"Error tagging image {image_id}: {e}")
190+
191+
return BatchOperationResponse(
192+
success=failed == 0,
193+
processed=processed,
194+
failed=failed,
195+
errors=errors
196+
)
197+
198+
199+
@router.post("/batch-move", response_model=BatchOperationResponse)
200+
def batch_move_to_album(request: BatchMoveRequest):
201+
"""Move multiple images to an album"""
202+
processed = 0
203+
failed = 0
204+
errors = []
205+
206+
# TODO: Implement album functionality
207+
for image_id in request.image_ids:
208+
try:
209+
# Placeholder for album move functionality
210+
# success = db_add_image_to_album(image_id, request.album_id)
211+
processed += 1
212+
except Exception as e:
213+
failed += 1
214+
errors.append(f"Failed to move {image_id}: {str(e)}")
215+
logger.error(f"Error moving image {image_id}: {e}")
216+
217+
return BatchOperationResponse(
218+
success=failed == 0,
219+
processed=processed,
220+
failed=failed,
221+
errors=errors
222+
)

backend/app/schemas/batch.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pydantic import BaseModel
2+
from typing import List
3+
4+
class BatchDeleteRequest(BaseModel):
5+
"""Request model for batch delete operation"""
6+
image_ids: List[str]
7+
8+
class BatchTagRequest(BaseModel):
9+
"""Request model for batch tag operation"""
10+
image_ids: List[str]
11+
tags: List[str]
12+
13+
class BatchMoveRequest(BaseModel):
14+
"""Request model for batch move to album operation"""
15+
image_ids: List[str]
16+
album_id: str
17+
18+
class BatchOperationResponse(BaseModel):
19+
"""Response model for batch operations"""
20+
success: bool
21+
processed: int
22+
failed: int
23+
errors: List[str] = []

0 commit comments

Comments
 (0)