Major Features: - 🖥️ Standalone desktop app (VideoMatcher.app) - double-click to run - 🎨 Black & gold branded UI (Montserrat font, #FFC407 accent) - 📁 Local file browser for master/adaptation folders - ⚡ Fast mode processing (10-20x faster, disables AKAZE/AI Vision) - 🤖 Smart AI Vision fallback (auto-retry when no matches found) - 📊 Real-time progress bars (fingerprinting & matching) - 💾 Local processing (no cloud, no authentication) - 📤 CSV export with master filenames Web Application (Enterprise): - 🌐 Flask web app with Azure AD authentication - 📦 Box.com integration for cloud storage - 🐳 Docker support for deployment - 🔐 JWT validation with httpOnly cookies - 🎯 REST API endpoints Enhancements: - Fixed master filename lookup (was showing "Unknown") - Automatic fingerprint recovery (detects missing files) - Improved CSV format (master file next to adaptation) - Port conflict handling (auto-finds available port) - Environment variable fixes for standalone mode Documentation: - Updated README with standalone app section - Added 10+ guide documents (UI improvements, fingerprint recovery, etc.) - Build instructions with PyInstaller - Comprehensive troubleshooting guide Technical: - PyInstaller build configuration (video_matcher.spec) - Launcher with environment setup (launcher.py) - Mock authentication for standalone mode - Video matcher service layer - Metadata parser and AKAZE video matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
168 lines
4.9 KiB
Python
Executable file
168 lines
4.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Video Matcher Standalone Launcher
|
|
Starts local Flask server and opens browser automatically
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import webbrowser
|
|
import socket
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
# CRITICAL: Set environment variables BEFORE any other imports
|
|
# This ensures standalone mode is activated before Flask app initializes
|
|
os.environ['STANDALONE_MODE'] = '1'
|
|
os.environ['DISABLE_AUTH'] = '1'
|
|
|
|
# Add project root to path
|
|
PROJECT_ROOT = Path(__file__).parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
def check_server_running(host, port):
|
|
"""Check if a server is already running on the specified port"""
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(1)
|
|
s.connect((host, port))
|
|
return True
|
|
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
return False
|
|
|
|
def find_free_port(start_port=5000, max_attempts=10):
|
|
"""Find an available port starting from start_port"""
|
|
for port in range(start_port, start_port + max_attempts):
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(('127.0.0.1', port))
|
|
return port
|
|
except OSError:
|
|
continue
|
|
raise RuntimeError(f"Could not find free port in range {start_port}-{start_port + max_attempts}")
|
|
|
|
def wait_for_server(host, port, timeout=10):
|
|
"""Wait for server to be ready"""
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout:
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(1)
|
|
s.connect((host, port))
|
|
return True
|
|
except (socket.timeout, ConnectionRefusedError):
|
|
time.sleep(0.1)
|
|
return False
|
|
|
|
def open_browser(url, delay=1.5):
|
|
"""Open browser after a short delay"""
|
|
time.sleep(delay)
|
|
try:
|
|
webbrowser.open(url)
|
|
print(f"✓ Opened browser at {url}")
|
|
except Exception as e:
|
|
print(f"⚠ Could not open browser automatically: {e}")
|
|
print(f" Please open your browser and navigate to: {url}")
|
|
|
|
def setup_environment():
|
|
"""Setup environment variables for standalone mode"""
|
|
# Environment variables already set at module level, but ensure they're set
|
|
os.environ.setdefault('STANDALONE_MODE', '1')
|
|
os.environ.setdefault('DISABLE_AUTH', '1')
|
|
|
|
# Use local data directory
|
|
data_dir = PROJECT_ROOT / 'data'
|
|
data_dir.mkdir(exist_ok=True)
|
|
|
|
# Ensure required directories exist
|
|
(data_dir / 'fingerprints').mkdir(exist_ok=True)
|
|
(data_dir / 'jobs').mkdir(exist_ok=True)
|
|
|
|
# Set temp directory for downloads (if using Box)
|
|
temp_dir = PROJECT_ROOT / 'tmp' / 'video_downloads'
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
os.environ['VIDEO_TEMP_DIR'] = str(temp_dir)
|
|
|
|
print(f"✓ Data directory: {data_dir}")
|
|
print(f"✓ Temp directory: {temp_dir}")
|
|
|
|
def main():
|
|
"""Main launcher function"""
|
|
print("=" * 60)
|
|
print(" VIDEO MATCHER - Standalone Application")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
# Setup environment
|
|
print("Setting up environment...")
|
|
setup_environment()
|
|
print()
|
|
|
|
# Find available port (skip check for existing server, always start fresh)
|
|
host = '127.0.0.1'
|
|
|
|
try:
|
|
port = find_free_port()
|
|
url = f"http://{host}:{port}"
|
|
print(f"✓ Starting server on port: {port}")
|
|
print()
|
|
except RuntimeError as e:
|
|
print(f"✗ Error: {e}")
|
|
input("Press Enter to exit...")
|
|
sys.exit(1)
|
|
|
|
# Import Flask app
|
|
try:
|
|
from app import app
|
|
print("✓ Application loaded successfully")
|
|
print()
|
|
except Exception as e:
|
|
print(f"✗ Error loading application: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
input("Press Enter to exit...")
|
|
sys.exit(1)
|
|
|
|
# Start browser opener in background thread
|
|
browser_thread = threading.Thread(target=open_browser, args=(url,), daemon=True)
|
|
browser_thread.start()
|
|
|
|
# Start Flask server
|
|
print(f"Starting server at {url}")
|
|
print()
|
|
print("=" * 60)
|
|
print(" APPLICATION RUNNING")
|
|
print("=" * 60)
|
|
print(f" URL: {url}")
|
|
print(f" Press Ctrl+C to stop the server")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
try:
|
|
# Disable Flask reloader in standalone mode
|
|
app.run(
|
|
host=host,
|
|
port=port,
|
|
debug=False,
|
|
use_reloader=False,
|
|
threaded=True
|
|
)
|
|
except KeyboardInterrupt:
|
|
print("\n\n✓ Server stopped by user")
|
|
except Exception as e:
|
|
print(f"\n\n✗ Server error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
input("Press Enter to exit...")
|
|
sys.exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
print(f"\n\n✗ Fatal error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
input("Press Enter to exit...")
|
|
sys.exit(1)
|