This commit introduces a comprehensive set of modules for the PunimTag application, including configuration management, database operations, face processing, photo management, and tag management. Each module is designed to encapsulate specific functionalities, enhancing maintainability and scalability. The GUI components are also integrated, allowing for a cohesive user experience. This foundational work sets the stage for future enhancements and features, ensuring a robust framework for photo tagging and face recognition tasks.
218 lines
8.4 KiB
Python
218 lines
8.4 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
|
|
|
|
|
|
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
|