feat: Enhance installation script and documentation for Python tkinter support

This commit updates the `install.sh` script to include the installation of Python tkinter, which is required for the native folder picker functionality. Additionally, the README.md is modified to reflect this new requirement, providing installation instructions for various operating systems. The documentation is further enhanced with troubleshooting tips for users encountering issues with the folder picker, ensuring a smoother setup experience.
This commit is contained in:
Tanya 2026-01-06 12:29:40 -05:00
parent 1f3f35d535
commit 906e2cbe19
4 changed files with 179 additions and 19 deletions

View File

@ -23,6 +23,8 @@ A fast, simple, and modern web application for organizing and tagging photos usi
- **⚡ Batch Processing**: Process thousands of photos efficiently
- **🎯 Unique Faces Filter**: Hide duplicate faces to focus on unique individuals
- **🔄 Real-time Updates**: Live progress tracking and job status updates
- **🌐 Network Path Support**: Browse and scan folders on network shares (UNC paths on Windows, mounted shares on Linux)
- **📁 Native Folder Picker**: Browse button uses native OS folder picker with full absolute path support
- **🔒 Privacy-First**: All data stored locally, no cloud dependencies
---
@ -35,8 +37,9 @@ A fast, simple, and modern web application for organizing and tagging photos usi
- **Node.js 18+ and npm**
- **PostgreSQL** (required for both development and production)
- **Redis** (for background job processing)
- **Python tkinter** (for native folder picker in Scan tab)
**Note:** The automated installation script (`./install.sh`) will install PostgreSQL and Redis automatically on Ubuntu/Debian systems.
**Note:** The automated installation script (`./install.sh`) will install PostgreSQL, Redis, and Python tkinter automatically on Ubuntu/Debian systems.
### Installation
@ -55,7 +58,7 @@ cd punimtag
The script will:
- ✅ Check prerequisites (Python 3.12+, Node.js 18+)
- ✅ Install system dependencies (PostgreSQL, Redis) on Ubuntu/Debian
- ✅ Install system dependencies (PostgreSQL, Redis, Python tkinter) on Ubuntu/Debian
- ✅ Set up PostgreSQL databases (main + auth)
- ✅ Create Python virtual environment
- ✅ Install all Python dependencies
@ -69,7 +72,13 @@ cd viewer-frontend
npx prisma generate
```
**Note:** On macOS or other systems, the script will skip system dependency installation. You'll need to install PostgreSQL and Redis manually.
**Note:** On macOS or other systems, the script will skip system dependency installation. You'll need to install PostgreSQL, Redis, and Python tkinter manually.
**Installing tkinter manually:**
- **Ubuntu/Debian:** `sudo apt install python3-tk`
- **RHEL/CentOS:** `sudo yum install python3-tkinter`
- **macOS:** Usually included with Python, but if missing: `brew install python-tk` (if using Homebrew Python)
- **Windows:** Usually included with Python installation
#### Option 2: Manual Installation
@ -375,6 +384,26 @@ rm data/punimtag.db
# The schema will be recreated on next startup
```
**Browse button returns 503 error or doesn't show folder picker:**
This indicates that Python tkinter is not available. Install it:
```bash
# Ubuntu/Debian:
sudo apt install python3-tk
# RHEL/CentOS:
sudo yum install python3-tkinter
# Verify installation:
python3 -c "import tkinter; print('tkinter available')"
```
**Note:** If running on a remote server without a display, you may need to set the DISPLAY environment variable or use X11 forwarding:
```bash
export DISPLAY=:0
# Or for X11 forwarding:
export DISPLAY=localhost:10.0
```
**Viewer frontend shows 0 photos:**
- Make sure the database has photos (import them via admin frontend)
- Verify `DATABASE_URL` in `viewer-frontend/.env` points to the correct database
@ -492,6 +521,9 @@ punimtag/
**Frontend:**
- ✅ Scan tab UI with folder selection
- ✅ **Native folder picker (Browse button)** - Uses tkinter for native OS folder selection
- ✅ **Network path support** - Handles UNC paths (Windows: `\\server\share\folder`) and mounted network shares (Linux: `/mnt/nfs-share/photos`)
- ✅ **Full absolute path handling** - Automatically normalizes and validates paths
- ✅ Drag-and-drop file upload
- ✅ Recursive scan toggle
- ✅ Real-time job progress with progress bar

View File

@ -15,6 +15,7 @@ export default function Scan() {
const [folderPath, setFolderPath] = useState('')
const [recursive, setRecursive] = useState(true)
const [isImporting, setIsImporting] = useState(false)
const [isBrowsing, setIsBrowsing] = useState(false)
const [currentJob, setCurrentJob] = useState<JobResponse | null>(null)
const [jobProgress, setJobProgress] = useState<JobProgress | null>(null)
const [importResult, setImportResult] = useState<{
@ -35,43 +36,115 @@ export default function Scan() {
}, [])
const handleFolderBrowse = async () => {
setIsBrowsing(true)
setError(null)
// Try backend API first (uses tkinter for native folder picker with full path)
try {
console.log('Attempting to open native folder picker...')
const result = await photosApi.browseFolder()
console.log('Backend folder picker result:', result)
if (result.success && result.path) {
setFolderPath(result.path)
return
// Ensure we have a valid absolute path (not just folder name)
const path = result.path.trim()
if (path && path.length > 0) {
// Verify it looks like an absolute path:
// - Unix/Linux: starts with / (includes mounted network shares like /mnt/...)
// - Windows local: starts with drive letter like C:\
// - Windows UNC: starts with \\ (network paths like \\server\share\folder)
const isUnixPath = path.startsWith('/')
const isWindowsLocalPath = /^[A-Za-z]:[\\/]/.test(path)
const isWindowsUncPath = path.startsWith('\\\\') || path.startsWith('//')
if (isUnixPath || isWindowsLocalPath || isWindowsUncPath) {
setFolderPath(path)
setIsBrowsing(false)
return
} else {
// Backend validated it, so trust it even if it doesn't match our patterns
// (might be a valid path format we didn't account for)
console.warn('Backend returned path with unexpected format:', path)
setFolderPath(path)
setIsBrowsing(false)
return
}
}
}
// If we get here, result.success was false or path was empty
console.warn('Backend folder picker returned no path:', result)
if (result.success === false && result.message) {
setError(result.message || 'No folder was selected. Please try again.')
} else {
setError('No folder was selected. Please try again.')
}
setIsBrowsing(false)
} catch (err: any) {
// Backend API failed, fall back to browser picker
console.warn('Backend folder picker unavailable, using browser fallback:', err)
// Extract error message from various possible locations
const errorMsg = err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.message ||
String(err) ||
''
console.log('Error details:', {
status: err?.response?.status,
detail: err?.response?.data?.detail,
message: err?.message,
fullError: err
})
// Check if it's a display/availability issue
const errorMsg = err?.response?.data?.detail || err?.message || ''
if (errorMsg.includes('display') || errorMsg.includes('DISPLAY')) {
if (errorMsg.includes('display') || errorMsg.includes('DISPLAY') || errorMsg.includes('tkinter')) {
// Show user-friendly message about display issue
alert('Native folder picker unavailable (no display). Using browser fallback.\n\nNote: Browser picker may only show folder name. You may need to manually complete the full path.')
setError('Native folder picker unavailable. Using browser fallback.')
} else if (err?.response?.status === 503) {
// 503 Service Unavailable - likely tkinter or display issue
setError('Native folder picker unavailable (tkinter/display issue). Using browser fallback.')
} else {
// Other error - log it but continue to browser fallback
console.error('Error calling backend folder picker:', err)
setError('Native folder picker unavailable. Using browser fallback.')
}
}
// Fallback: Use browser-based folder picker
// This code runs if backend API failed or returned no path
console.log('Attempting browser fallback folder picker...')
// Use File System Access API if available (modern browsers)
if (typeof window !== 'undefined' && 'showDirectoryPicker' in window) {
try {
console.log('Using File System Access API...')
const directoryHandle = await (window as any).showDirectoryPicker()
// Get the folder name from the handle
const folderName = directoryHandle.name
// Note: Browsers don't expose full absolute paths for security reasons
console.log('Selected folder name:', folderName)
// Browser picker only gives folder name, not full path
// Set the folder name and show helpful message
setFolderPath(folderName)
setError('Browser picker only shows folder name. Please enter the full absolute path manually in the input field above.')
} catch (err: any) {
// User cancelled the picker
if (err.name !== 'AbortError') {
console.error('Error selecting folder:', err)
setError('Error opening folder picker: ' + err.message)
} else {
// User cancelled - clear any previous error
setError(null)
}
} finally {
setIsBrowsing(false)
}
} else {
// Fallback: use a hidden directory input
// Note: This will show a browser confirmation dialog that cannot be removed
console.log('Using file input fallback...')
const input = document.createElement('input')
input.type = 'file'
input.setAttribute('webkitdirectory', '')
@ -87,17 +160,24 @@ export default function Scan() {
const pathParts = relativePath.split('/')
const rootFolder = pathParts[0]
// Note: Browsers don't expose full absolute paths for security reasons
console.log('Selected folder name:', rootFolder)
// Browser picker only gives folder name, not full path
// Set the folder name and show helpful message
setFolderPath(rootFolder)
setError('Browser picker only shows folder name. Please enter the full absolute path manually in the input field above.')
}
if (document.body.contains(input)) {
document.body.removeChild(input)
}
setIsBrowsing(false)
}
input.oncancel = () => {
if (document.body.contains(input)) {
document.body.removeChild(input)
}
setIsBrowsing(false)
}
document.body.appendChild(input)
@ -252,18 +332,14 @@ export default function Scan() {
<button
type="button"
onClick={handleFolderBrowse}
disabled={isImporting}
disabled={isImporting || isBrowsing}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Browse
{isBrowsing ? 'Opening...' : 'Browse'}
</button>
</div>
<p className="mt-1 text-sm text-gray-500">
Enter the full path to the folder containing photos / videos.
<span className="text-xs text-gray-400 block mt-1">
Click Browse to open a native folder picker. The full path will be automatically filled.
If the native picker is unavailable, a browser fallback will be used (may require manual path completion).
</span>
Enter the full absolute path to the folder containing photos / videos.
</p>
</div>

View File

@ -466,12 +466,55 @@ def browse_folder() -> dict:
root.destroy()
if folder_path:
# Normalize path to absolute
abs_path = os.path.abspath(folder_path)
# Normalize path to absolute - use multiple methods to ensure full path
# Handle network paths (UNC paths on Windows, mounted shares on Linux)
# Check if it's a Windows UNC path (\\server\share or //server/share)
# UNC paths are already absolute, but realpath may not work correctly with them
is_unc_path = folder_path.startswith('\\\\') or folder_path.startswith('//')
if is_unc_path:
# For UNC paths, normalize separators but don't use realpath
# (realpath may not work correctly with UNC paths on Windows)
# os.path.normpath() handles UNC paths correctly on Windows
normalized_path = os.path.normpath(folder_path)
else:
# For regular paths (local or mounted network shares on Linux)
# First convert to absolute, then resolve any symlinks, then normalize
abs_path = os.path.abspath(folder_path)
try:
# Resolve any symlinks to get the real path
# This may fail for some network paths, so wrap in try/except
real_path = os.path.realpath(abs_path)
except (OSError, ValueError):
# If realpath fails (e.g., for some network paths), use abspath result
real_path = abs_path
# Normalize the path (remove redundant separators, etc.)
normalized_path = os.path.normpath(real_path)
# Ensure we have a full absolute path (not just folder name)
if not os.path.isabs(normalized_path):
# If somehow still not absolute, try again with current working directory
normalized_path = os.path.abspath(normalized_path)
# Verify the path exists and is a directory
# Note: For network paths, this check might fail if network is temporarily down,
# but if the user just selected it via the folder picker, it should be accessible
if not os.path.exists(normalized_path):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Selected path does not exist: {normalized_path}",
)
if not os.path.isdir(normalized_path):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Selected path is not a directory: {normalized_path}",
)
return {
"path": abs_path,
"path": normalized_path,
"success": True,
"message": f"Selected folder: {abs_path}"
"message": f"Selected folder: {normalized_path}"
}
else:
return {

View File

@ -118,6 +118,15 @@ install_system_dependencies() {
echo -e "${GREEN} ✅ Redis already installed${NC}"
fi
# Install Python tkinter (required for native folder picker)
if ! python3 -c "import tkinter" 2>/dev/null; then
echo -e "${BLUE} Installing Python tkinter...${NC}"
sudo apt install -y python3-tk
echo -e "${GREEN} ✅ Python tkinter installed${NC}"
else
echo -e "${GREEN} ✅ Python tkinter already installed${NC}"
fi
# Verify Redis is running
if redis-cli ping >/dev/null 2>&1; then
echo -e "${GREEN} ✅ Redis is running${NC}"