punimtag/photo_management.py
tanyar09 36aaadca1d Add folder browsing and path validation features to Dashboard GUI and photo management
This commit introduces a folder browsing button in the Dashboard GUI, allowing users to select a folder for photo scanning. It also implements path normalization and validation using new utility functions from the path_utils module, ensuring that folder paths are absolute and accessible before scanning. Additionally, the PhotoManager class has been updated to utilize these path utilities, enhancing the robustness of folder scanning operations. This improves user experience by preventing errors related to invalid paths and streamlining folder management across the application.
2025-10-09 12:43:28 -04:00

229 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""
Photo scanning, metadata extraction, and file operations for PunimTag
"""
import os
from pathlib import Path
from PIL import Image
from datetime import datetime
from typing import Optional, List, Tuple
from config import SUPPORTED_IMAGE_FORMATS
from database import DatabaseManager
from path_utils import normalize_path, validate_path_exists
class PhotoManager:
"""Handles photo scanning, metadata extraction, and file operations"""
def __init__(self, db_manager: DatabaseManager, verbose: int = 0):
"""Initialize photo manager"""
self.db = db_manager
self.verbose = verbose
def extract_photo_date(self, photo_path: str) -> Optional[str]:
"""Extract date taken from photo EXIF data"""
try:
with Image.open(photo_path) as image:
exifdata = image.getexif()
# Look for date taken in EXIF tags
date_tags = [
306, # DateTime
36867, # DateTimeOriginal
36868, # DateTimeDigitized
]
for tag_id in date_tags:
if tag_id in exifdata:
date_str = exifdata[tag_id]
if date_str:
# Parse EXIF date format (YYYY:MM:DD HH:MM:SS)
try:
date_obj = datetime.strptime(date_str, '%Y:%m:%d %H:%M:%S')
return date_obj.strftime('%Y-%m-%d')
except ValueError:
# Try alternative format
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
return date_obj.strftime('%Y-%m-%d')
except ValueError:
continue
return None
except Exception as e:
if self.verbose >= 2:
print(f" ⚠️ Could not extract date from {os.path.basename(photo_path)}: {e}")
return None
def scan_folder(self, folder_path: str, recursive: bool = True) -> int:
"""Scan folder for photos and add to database"""
# Normalize path to absolute path
try:
folder_path = normalize_path(folder_path)
except ValueError as e:
print(f"❌ Invalid path: {e}")
return 0
if not validate_path_exists(folder_path):
print(f"❌ Folder not found or not accessible: {folder_path}")
return 0
found_photos = []
if recursive:
for root, dirs, files in os.walk(folder_path):
for file in files:
file_ext = Path(file).suffix.lower()
if file_ext in SUPPORTED_IMAGE_FORMATS:
photo_path = os.path.join(root, file)
found_photos.append((photo_path, file))
else:
for file in os.listdir(folder_path):
file_ext = Path(file).suffix.lower()
if file_ext in SUPPORTED_IMAGE_FORMATS:
photo_path = os.path.join(folder_path, file)
found_photos.append((photo_path, file))
if not found_photos:
print(f"📁 No photos found in {folder_path}")
return 0
# Add to database
added_count = 0
existing_count = 0
for photo_path, filename in found_photos:
try:
# Ensure photo path is absolute
photo_path = normalize_path(photo_path)
# Extract date taken from EXIF data
date_taken = self.extract_photo_date(photo_path)
# Add photo to database (with absolute path)
photo_id = self.db.add_photo(photo_path, filename, date_taken)
if photo_id:
# New photo was added
added_count += 1
if self.verbose >= 2:
date_info = f" (taken: {date_taken})" if date_taken else " (no date)"
print(f" 📸 Added: {filename}{date_info}")
else:
# Photo already exists
existing_count += 1
if self.verbose >= 2:
print(f" 📸 Already exists: {filename}")
except Exception as e:
print(f"⚠️ Error adding {filename}: {e}")
# Print summary
if added_count > 0 and existing_count > 0:
print(f"📁 Found {len(found_photos)} photos: {added_count} new, {existing_count} already in database")
elif added_count > 0:
print(f"📁 Found {len(found_photos)} photos, added {added_count} new photos")
elif existing_count > 0:
print(f"📁 Found {len(found_photos)} photos, all already in database")
else:
print(f"📁 Found {len(found_photos)} photos, none could be added")
return added_count
def get_photo_info(self, photo_id: int) -> Optional[Tuple]:
"""Get photo information by ID"""
photos = self.db.get_photos_by_pattern(limit=1000) # Get all photos
for photo in photos:
if photo[0] == photo_id: # photo[0] is the ID
return photo
return None
def get_photo_path(self, photo_id: int) -> Optional[str]:
"""Get photo path by ID"""
photo_info = self.get_photo_info(photo_id)
return photo_info[1] if photo_info else None # photo[1] is the path
def get_photo_filename(self, photo_id: int) -> Optional[str]:
"""Get photo filename by ID"""
photo_info = self.get_photo_info(photo_id)
return photo_info[2] if photo_info else None # photo[2] is the filename
def is_photo_processed(self, photo_id: int) -> bool:
"""Check if photo has been processed for faces"""
photo_info = self.get_photo_info(photo_id)
return photo_info[4] if photo_info else False # photo[4] is the processed flag
def mark_photo_processed(self, photo_id: int):
"""Mark a photo as processed"""
self.db.mark_photo_processed(photo_id)
def get_photos_by_date_range(self, date_from: str = None, date_to: str = None) -> List[Tuple]:
"""Get photos within a date range"""
# This would need to be implemented in the database module
# For now, return all photos
return self.db.get_photos_by_pattern()
def get_photos_by_pattern(self, pattern: str = None, limit: int = 10) -> List[Tuple]:
"""Get photos matching a pattern"""
return self.db.get_photos_by_pattern(pattern, limit)
def validate_photo_file(self, photo_path: str) -> bool:
"""Validate that a photo file exists and is readable"""
if not os.path.exists(photo_path):
return False
try:
with Image.open(photo_path) as image:
image.verify()
return True
except Exception:
return False
def get_photo_dimensions(self, photo_path: str) -> Optional[Tuple[int, int]]:
"""Get photo dimensions (width, height)"""
try:
with Image.open(photo_path) as image:
return image.size
except Exception:
return None
def get_photo_format(self, photo_path: str) -> Optional[str]:
"""Get photo format"""
try:
with Image.open(photo_path) as image:
return image.format
except Exception:
return None
def get_photo_exif_data(self, photo_path: str) -> dict:
"""Get EXIF data from photo"""
try:
with Image.open(photo_path) as image:
exifdata = image.getexif()
return dict(exifdata)
except Exception:
return {}
def get_photo_file_size(self, photo_path: str) -> Optional[int]:
"""Get photo file size in bytes"""
try:
return os.path.getsize(photo_path)
except Exception:
return None
def get_photo_creation_time(self, photo_path: str) -> Optional[datetime]:
"""Get photo file creation time"""
try:
timestamp = os.path.getctime(photo_path)
return datetime.fromtimestamp(timestamp)
except Exception:
return None
def get_photo_modification_time(self, photo_path: str) -> Optional[datetime]:
"""Get photo file modification time"""
try:
timestamp = os.path.getmtime(photo_path)
return datetime.fromtimestamp(timestamp)
except Exception:
return None