From 6bfc44a6c9f3e8b027e5539a685421b0b3ffb9e3 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Wed, 1 Oct 2025 15:10:23 -0400 Subject: [PATCH] Refactor database schema for tag management in PhotoTagger Enhance the tagging system by introducing a normalized structure with a separate `tags` table for unique tag definitions and a `phototaglinkage` table to manage the many-to-many relationship between photos and tags. Update the logic for inserting and retrieving tags to improve data integrity and prevent duplicates. Additionally, update the README to reflect these changes and document the new folder view features. --- README.md | 25 +++++++++++++++++++++++-- photo_tagger.py | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f723bf5..763bed6 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,8 @@ python3 photo_tagger.py tag-manager This GUI provides a file explorer-like interface for managing photo tags with advanced column resizing and multiple view modes. **🎯 Tag Manager Features:** -- 📊 **Multiple View Modes** - List view, icon view, and compact view for different needs +- 📊 **Multiple View Modes** - List view, icon view, compact view, and folder view for different needs +- 📁 **Folder Grouping** - Group photos by directory with expandable/collapsible folders - 🔧 **Resizable Columns** - Drag column separators to resize both headers and data rows - 👁️ **Column Visibility** - Right-click to show/hide columns in each view mode - 🖼️ **Thumbnail Display** - Icon view shows photo thumbnails with metadata @@ -211,6 +212,14 @@ This GUI provides a file explorer-like interface for managing photo tags with ad - ⚡ **Fast Loading** - Minimal data for quick browsing - 🎯 **Focused Display** - Perfect for quick tag management +**Folder View:** +- 📁 **Directory Grouping** - Photos grouped by their directory path +- 🔽 **Expandable Folders** - Click folder headers to expand/collapse +- 📊 **Photo Counts** - Shows number of photos in each folder +- 🎯 **File Explorer Style** - Familiar tree-like interface +- 📄 **Photo Details** - Shows filename, processed status, date taken, face count, and tags +- 🖱️ **Easy Navigation** - Click anywhere on folder header to toggle + **🔧 Column Resizing:** - 🖱️ **Drag to Resize** - Click and drag red separators between columns - 📏 **Minimum Width** - Columns maintain minimum 50px width @@ -224,6 +233,15 @@ This GUI provides a file explorer-like interface for managing photo tags with ad - 🎯 **View-Specific** - Column settings saved per view mode - 🔄 **Instant Updates** - Changes apply immediately +**📁 Folder View Usage:** +- 🖱️ **Click Folder Headers** - Click anywhere on a folder row to expand/collapse +- 🔽 **Expand/Collapse Icons** - ▶ indicates collapsed, ▼ indicates expanded +- 📊 **Photo Counts** - Each folder shows "(X photos)" in the header +- 🎯 **Root Directory** - Photos without a directory path are grouped under "Root" +- 📁 **Alphabetical Sorting** - Folders are sorted alphabetically by directory name +- 🖼️ **Photo Details** - Expanded folders show all photos with their metadata +- 🔄 **Persistent State** - Folder expansion state is maintained while browsing + **Left Panel (People):** - 🔍 **Last Name Search** - Search box to filter people by last name (case-insensitive) - 🔎 **Search Button** - Apply filter to show only matching people @@ -365,7 +383,8 @@ The tool uses SQLite database (`data/photos.db` by default) with these tables: - **photos** - Photo file paths and processing status - **people** - Known people with separate first_name, last_name, and date_of_birth fields - **faces** - Face encodings, locations, and quality scores -- **tags** - Custom tags for photos +- **tags** - Tag definitions (unique tag names) +- **phototaglinkage** - Links between photos and tags (many-to-many relationship) - **person_encodings** - Face encodings for each person (for matching) ### Database Schema Improvements @@ -374,6 +393,8 @@ The tool uses SQLite database (`data/photos.db` by default) with these tables: - **Unique Constraint** - Prevents duplicate people with same name and birth date combination - **No Comma Issues** - Names are stored without commas, displayed as "Last, First" format - **Quality Scoring** - Faces table includes quality scores for better matching +- **Normalized Tag Structure** - Separate `tags` table for tag definitions and `phototaglinkage` table for photo-tag relationships +- **No Duplicate Tags** - Unique constraint prevents duplicate tag-photo combinations - **Optimized Queries** - Efficient indexing and query patterns for fast performance - **Data Integrity** - Proper foreign key relationships and constraints diff --git a/photo_tagger.py b/photo_tagger.py index 8aefaa6..ab475ad 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -204,14 +204,25 @@ class PhotoTagger: ) ''') - # Tags table + # Tags table - holds only tag information cursor.execute(''' CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, + tag_name TEXT UNIQUE NOT NULL, + created_date DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Photo-Tag linkage table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS phototaglinkage ( + linkage_id INTEGER PRIMARY KEY AUTOINCREMENT, photo_id INTEGER NOT NULL, - tag_name TEXT NOT NULL, + tag_id INTEGER NOT NULL, created_date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (photo_id) REFERENCES photos (id) + FOREIGN KEY (photo_id) REFERENCES photos (id), + FOREIGN KEY (tag_id) REFERENCES tags (id), + UNIQUE(photo_id, tag_id) ) ''') @@ -3150,10 +3161,22 @@ class PhotoTagger: if tags_input: tags = [tag.strip() for tag in tags_input.split(',') if tag.strip()] - for tag in tags: + for tag_name in tags: + # First, insert or get the tag_id from tags table cursor.execute( - 'INSERT INTO tags (photo_id, tag_name) VALUES (?, ?)', - (photo_id, tag) + 'INSERT OR IGNORE INTO tags (tag_name) VALUES (?)', + (tag_name,) + ) + cursor.execute( + 'SELECT id FROM tags WHERE tag_name = ?', + (tag_name,) + ) + tag_id = cursor.fetchone()[0] + + # Then, insert the linkage (ignore if already exists due to UNIQUE constraint) + cursor.execute( + 'INSERT OR IGNORE INTO phototaglinkage (photo_id, tag_id) VALUES (?, ?)', + (photo_id, tag_id) ) print(f" ✅ Added {len(tags)} tags") tagged_count += 1 @@ -3189,7 +3212,7 @@ class PhotoTagger: result = cursor.fetchone() stats['total_people'] = result[0] if result else 0 - cursor.execute('SELECT COUNT(DISTINCT tag_name) FROM tags') + cursor.execute('SELECT COUNT(*) FROM tags') result = cursor.fetchone() stats['unique_tags'] = result[0] if result else 0 @@ -4505,7 +4528,8 @@ class PhotoTagger: GROUP_CONCAT(DISTINCT t.tag_name) as tags FROM photos p LEFT JOIN faces f ON f.photo_id = p.id - LEFT JOIN tags t ON t.photo_id = p.id + LEFT JOIN phototaglinkage ptl ON ptl.photo_id = p.id + LEFT JOIN tags t ON t.id = ptl.tag_id GROUP BY p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added ORDER BY p.date_taken DESC, p.filename ''')