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.
This commit is contained in:
parent
150ae5fd3f
commit
36aaadca1d
@ -76,6 +76,16 @@ class DashboardGUI:
|
||||
folder_var = tk.StringVar()
|
||||
folder_entry = ttk.Entry(folder_row, textvariable=folder_var)
|
||||
folder_entry.pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)
|
||||
|
||||
# Browse button for folder selection
|
||||
def browse_folder():
|
||||
from tkinter import filedialog
|
||||
folder_path = filedialog.askdirectory(title="Select folder to scan for photos")
|
||||
if folder_path:
|
||||
folder_var.set(folder_path)
|
||||
|
||||
browse_btn = ttk.Button(folder_row, text="Browse", command=browse_folder)
|
||||
browse_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
# Recursive checkbox
|
||||
recursive_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(
|
||||
@ -93,8 +103,16 @@ class DashboardGUI:
|
||||
if not folder:
|
||||
messagebox.showwarning("Scan", "Please enter a folder path.", parent=root)
|
||||
return
|
||||
if not os.path.isdir(folder):
|
||||
messagebox.showerror("Scan", "Folder does not exist.", parent=root)
|
||||
|
||||
# Validate folder path using path utilities
|
||||
from path_utils import validate_path_exists, normalize_path
|
||||
try:
|
||||
folder = normalize_path(folder)
|
||||
if not validate_path_exists(folder):
|
||||
messagebox.showerror("Scan", f"Folder does not exist or is not accessible: {folder}", parent=root)
|
||||
return
|
||||
except ValueError as e:
|
||||
messagebox.showerror("Scan", f"Invalid folder path: {e}", parent=root)
|
||||
return
|
||||
if not callable(self.on_scan):
|
||||
messagebox.showinfo("Scan", "Scan is not wired yet.", parent=root)
|
||||
|
||||
174
path_utils.py
Normal file
174
path_utils.py
Normal file
@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path utility functions for PunimTag
|
||||
Ensures all paths are stored as absolute paths for consistency
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
|
||||
def normalize_path(path: Union[str, Path]) -> str:
|
||||
"""
|
||||
Convert any path to an absolute path.
|
||||
|
||||
Args:
|
||||
path: Path to normalize (can be relative or absolute)
|
||||
|
||||
Returns:
|
||||
Absolute path as string
|
||||
|
||||
Examples:
|
||||
normalize_path("demo_photos") -> "/home/user/punimtag/demo_photos"
|
||||
normalize_path("./photos") -> "/home/user/punimtag/photos"
|
||||
normalize_path("/absolute/path") -> "/absolute/path"
|
||||
"""
|
||||
if not path:
|
||||
raise ValueError("Path cannot be empty or None")
|
||||
|
||||
# Convert to string if Path object
|
||||
path_str = str(path)
|
||||
|
||||
# Use pathlib for robust path resolution
|
||||
normalized = Path(path_str).resolve()
|
||||
|
||||
return str(normalized)
|
||||
|
||||
|
||||
def is_absolute_path(path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Check if a path is absolute.
|
||||
|
||||
Args:
|
||||
path: Path to check
|
||||
|
||||
Returns:
|
||||
True if path is absolute, False if relative
|
||||
"""
|
||||
return Path(path).is_absolute()
|
||||
|
||||
|
||||
def is_relative_path(path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Check if a path is relative.
|
||||
|
||||
Args:
|
||||
path: Path to check
|
||||
|
||||
Returns:
|
||||
True if path is relative, False if absolute
|
||||
"""
|
||||
return not Path(path).is_absolute()
|
||||
|
||||
|
||||
def validate_path_exists(path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Check if a path exists and is accessible.
|
||||
|
||||
Args:
|
||||
path: Path to validate
|
||||
|
||||
Returns:
|
||||
True if path exists and is accessible, False otherwise
|
||||
"""
|
||||
try:
|
||||
normalized = normalize_path(path)
|
||||
return os.path.exists(normalized) and os.access(normalized, os.R_OK)
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def get_path_info(path: Union[str, Path]) -> dict:
|
||||
"""
|
||||
Get detailed information about a path.
|
||||
|
||||
Args:
|
||||
path: Path to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary with path information
|
||||
"""
|
||||
try:
|
||||
normalized = normalize_path(path)
|
||||
path_obj = Path(normalized)
|
||||
|
||||
return {
|
||||
'original': str(path),
|
||||
'normalized': normalized,
|
||||
'is_absolute': path_obj.is_absolute(),
|
||||
'is_relative': not path_obj.is_absolute(),
|
||||
'exists': path_obj.exists(),
|
||||
'is_file': path_obj.is_file(),
|
||||
'is_dir': path_obj.is_dir(),
|
||||
'parent': str(path_obj.parent),
|
||||
'name': path_obj.name,
|
||||
'stem': path_obj.stem,
|
||||
'suffix': path_obj.suffix
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'original': str(path),
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
|
||||
def ensure_directory_exists(path: Union[str, Path]) -> str:
|
||||
"""
|
||||
Ensure a directory exists, creating it if necessary.
|
||||
|
||||
Args:
|
||||
path: Directory path to ensure exists
|
||||
|
||||
Returns:
|
||||
Absolute path to the directory
|
||||
"""
|
||||
normalized = normalize_path(path)
|
||||
Path(normalized).mkdir(parents=True, exist_ok=True)
|
||||
return normalized
|
||||
|
||||
|
||||
def get_relative_path_from_base(absolute_path: Union[str, Path], base_path: Union[str, Path]) -> str:
|
||||
"""
|
||||
Get relative path from a base directory.
|
||||
|
||||
Args:
|
||||
absolute_path: Absolute path to convert
|
||||
base_path: Base directory to make relative to
|
||||
|
||||
Returns:
|
||||
Relative path from base directory
|
||||
"""
|
||||
abs_path = normalize_path(absolute_path)
|
||||
base = normalize_path(base_path)
|
||||
|
||||
try:
|
||||
return str(Path(abs_path).relative_to(base))
|
||||
except ValueError:
|
||||
# If paths don't share a common base, return the absolute path
|
||||
return abs_path
|
||||
|
||||
|
||||
# Test the utility functions
|
||||
if __name__ == "__main__":
|
||||
print("=== Path Utility Functions Test ===")
|
||||
|
||||
test_paths = [
|
||||
"demo_photos",
|
||||
"./demo_photos",
|
||||
"../demo_photos",
|
||||
"/home/ladmin/Code/punimtag/demo_photos",
|
||||
"C:/Users/Test/Photos",
|
||||
"/tmp/test"
|
||||
]
|
||||
|
||||
for path in test_paths:
|
||||
print(f"\nTesting: {path}")
|
||||
try:
|
||||
normalized = normalize_path(path)
|
||||
info = get_path_info(path)
|
||||
print(f" Normalized: {normalized}")
|
||||
print(f" Exists: {info['exists']}")
|
||||
print(f" Is directory: {info['is_dir']}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
@ -11,6 +11,7 @@ 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:
|
||||
@ -58,8 +59,15 @@ class PhotoManager:
|
||||
|
||||
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}")
|
||||
# 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 = []
|
||||
@ -88,10 +96,13 @@ class PhotoManager:
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
@ -382,7 +382,16 @@ Examples:
|
||||
if not args.target:
|
||||
print("❌ Please specify a folder to scan")
|
||||
return 1
|
||||
tagger.scan_folder(args.target, args.recursive)
|
||||
|
||||
# Normalize path to absolute path
|
||||
from path_utils import normalize_path
|
||||
try:
|
||||
normalized_path = normalize_path(args.target)
|
||||
print(f"📁 Scanning folder: {normalized_path}")
|
||||
tagger.scan_folder(normalized_path, args.recursive)
|
||||
except ValueError as e:
|
||||
print(f"❌ Invalid path: {e}")
|
||||
return 1
|
||||
|
||||
elif args.command == 'process':
|
||||
tagger.process_faces(args.limit, args.model)
|
||||
|
||||
@ -127,9 +127,15 @@ class SearchGUI:
|
||||
# Browse button for folder selection
|
||||
def browse_folder():
|
||||
from tkinter import filedialog
|
||||
from path_utils import normalize_path
|
||||
folder_path = filedialog.askdirectory(title="Select folder to filter by")
|
||||
if folder_path:
|
||||
folder_var.set(folder_path)
|
||||
try:
|
||||
# Normalize to absolute path
|
||||
normalized_path = normalize_path(folder_path)
|
||||
folder_var.set(normalized_path)
|
||||
except ValueError as e:
|
||||
messagebox.showerror("Invalid Path", f"Invalid folder path: {e}", parent=root)
|
||||
|
||||
browse_btn = ttk.Button(folder_filter_frame, text="Browse", command=browse_folder)
|
||||
browse_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
@ -140,6 +146,10 @@ class SearchGUI:
|
||||
|
||||
clear_folder_btn = ttk.Button(folder_filter_frame, text="Clear", command=clear_folder_filter)
|
||||
clear_folder_btn.pack(side=tk.LEFT, padx=(6, 0))
|
||||
|
||||
# Apply filters button
|
||||
apply_filters_btn = ttk.Button(filters_content, text="Apply filters", command=lambda: do_search())
|
||||
apply_filters_btn.pack(pady=(8, 0))
|
||||
|
||||
# Inputs area
|
||||
inputs = ttk.Frame(main)
|
||||
@ -428,9 +438,9 @@ class SearchGUI:
|
||||
filtered_results = []
|
||||
|
||||
for result in results:
|
||||
if len(result) >= 2:
|
||||
# Extract photo path from result tuple
|
||||
photo_path = result[1] if len(result) == 2 else result[0]
|
||||
if len(result) >= 1:
|
||||
# Extract photo path from result tuple (always at index 0)
|
||||
photo_path = result[0]
|
||||
|
||||
# Check if photo path starts with the specified folder path
|
||||
if photo_path.startswith(folder_path):
|
||||
@ -479,8 +489,8 @@ class SearchGUI:
|
||||
date_taken = get_photo_date_taken(path)
|
||||
tree.insert("", tk.END, values=("☐", "", photo_tags, "📁", "👤", path, date_taken))
|
||||
else:
|
||||
# For name search: (full_name, path) - show person column
|
||||
full_name, p = row
|
||||
# For name search: (path, full_name) - show person column
|
||||
p, full_name = row
|
||||
# Get tags for this photo
|
||||
photo_tags = get_photo_tags_for_display(p)
|
||||
date_taken = get_photo_date_taken(p)
|
||||
|
||||
@ -19,7 +19,7 @@ class SearchStats:
|
||||
def search_faces(self, person_name: str) -> List[Tuple[str, str]]:
|
||||
"""Search for photos containing a specific person by name (partial, case-insensitive).
|
||||
|
||||
Returns a list of tuples: (person_full_name, photo_path).
|
||||
Returns a list of tuples: (photo_path, person_full_name).
|
||||
"""
|
||||
# Get all people matching the name
|
||||
people = self.db.show_people_list()
|
||||
@ -70,7 +70,7 @@ class SearchStats:
|
||||
first = (row[1] or "").strip()
|
||||
last = (row[2] or "").strip()
|
||||
full_name = (f"{first} {last}").strip() or "Unknown"
|
||||
results.append((full_name, path))
|
||||
results.append((path, full_name))
|
||||
except Exception:
|
||||
# Fall back gracefully if schema differs
|
||||
pass
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user