#!/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 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""" if not os.path.exists(folder_path): print(f"❌ Folder not found: {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: # Extract date taken from EXIF data date_taken = self.extract_photo_date(photo_path) # Add photo to database 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