From 324a21800a71d16a0fccf544ca50cc7dcde9d3a0 Mon Sep 17 00:00:00 2001 From: Aodhan Date: Fri, 20 Jun 2025 22:27:24 +0100 Subject: [PATCH] Basic UI --- app.py | 282 ++++++++------ components/button.jsx | 1 + history.html | 857 ++---------------------------------------- index.html | 94 +++-- js/history.js | 186 +++++++++ js/main.js | 194 ++++++++++ js/utils.js | 14 + script.js | 635 ------------------------------- sort_images.py | 85 +++++ styles.css | 540 +++++++++++++------------- 10 files changed, 1022 insertions(+), 1866 deletions(-) create mode 100644 components/button.jsx create mode 100644 js/history.js create mode 100644 js/main.js create mode 100644 js/utils.js delete mode 100644 script.js create mode 100644 sort_images.py diff --git a/app.py b/app.py index 8a7a685..14bcddf 100644 --- a/app.py +++ b/app.py @@ -21,53 +21,42 @@ def init_db(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() - # Check if orientation column exists - cursor.execute("PRAGMA table_info(image_selections)") - columns = [column[1] for column in cursor.fetchall()] - - # Create table if it doesn't exist - if 'image_selections' not in [table[0] for table in cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()]: - cursor.execute(''' - CREATE TABLE IF NOT EXISTS image_selections ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - image_path TEXT NOT NULL, - resolution TEXT NOT NULL, - action TEXT NOT NULL, - timestamp INTEGER NOT NULL, - orientation TEXT - ) - ''') - print("Created new image_selections table with orientation column") - elif 'orientation' not in columns: - # Add orientation column if it doesn't exist - cursor.execute('ALTER TABLE image_selections ADD COLUMN orientation TEXT') - print("Added orientation column to existing table") + # Create image_selections table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS image_selections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + image_path TEXT NOT NULL UNIQUE, + action TEXT NOT NULL, + timestamp INTEGER NOT NULL + ) + ''') + + # Create image_metadata table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS image_metadata ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT NOT NULL UNIQUE, + resolution TEXT NOT NULL, + name TEXT NOT NULL, + orientation TEXT NOT NULL, + discovered_at INTEGER NOT NULL + ) + ''') conn.commit() conn.close() print(f"Database initialized at {DB_PATH}") # Add a selection to the database -def add_selection(image_path, resolution, action): - # Determine if image is portrait or landscape - orientation = "unknown" - try: - from PIL import Image - full_path = os.path.join(IMAGE_DIR, image_path.replace('/images/', '')) - with Image.open(full_path) as img: - width, height = img.size - orientation = "portrait" if height > width else "landscape" if width > height else "square" - except Exception as e: - print(f"DEBUG ERROR determining image orientation: {str(e)}") - +def add_selection(image_path, action): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() - # Insert the selection + # Use REPLACE INTO to handle potential duplicates gracefully cursor.execute(''' - INSERT INTO image_selections (image_path, resolution, action, timestamp, orientation) - VALUES (?, ?, ?, ?, ?) - ''', (image_path, resolution, action, int(time.time()), orientation)) + REPLACE INTO image_selections (image_path, action, timestamp) + VALUES (?, ?, ?) + ''', (image_path, action, int(time.time()))) conn.commit() conn.close() @@ -98,7 +87,6 @@ def get_selections(): if 'orientation' not in item or not item['orientation']: try: # Try to determine orientation if not in database - from PIL import Image image_path = item['image_path'] if image_path.startswith('/images/'): image_path = image_path[8:] @@ -123,23 +111,65 @@ def get_selections(): return [] # Get a list of all image paths that have already been actioned -def get_actioned_images(): - try: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - cursor.execute(''' - SELECT DISTINCT image_path FROM image_selections - ''') - - rows = cursor.fetchall() - actioned_images = [row[0] for row in rows] - +def sync_image_database(): + """Scans the image directory and adds any new images to the metadata table.""" + print("Syncing image database...") + from PIL import Image + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Get all image paths already in the database + cursor.execute("SELECT path FROM image_metadata") + db_images = {row[0] for row in cursor.fetchall()} + print(f"Found {len(db_images)} images in the database.") + + # Find all images on the filesystem + disk_images = set() + resolutions = [d for d in os.listdir(IMAGE_DIR) if os.path.isdir(os.path.join(IMAGE_DIR, d))] + for res in resolutions: + res_dir = os.path.join(IMAGE_DIR, res) + for img_name in os.listdir(res_dir): + if img_name.lower().endswith(('.png', '.jpg', '.jpeg')): + disk_images.add(f"{res}/{img_name}") + print(f"Found {len(disk_images)} images on disk.") + + # Determine which images are new + new_images = disk_images - db_images + print(f"Found {len(new_images)} new images to add to the database.") + + if not new_images: + print("Database is already up-to-date.") conn.close() - return actioned_images - except Exception as e: - print(f"DEBUG ERROR in get_actioned_images(): {str(e)}") - return [] + return + + # Process and add new images to the database + images_to_add = [] + total_new_images = len(new_images) + processed_count = 0 + for image_path in new_images: + res, img_name = image_path.split('/', 1) + full_path = os.path.join(IMAGE_DIR, image_path) + try: + with Image.open(full_path) as img: + width, height = img.size + orientation = 'landscape' if width >= height else 'portrait' + images_to_add.append((image_path, res, img_name, orientation, int(time.time()))) + processed_count += 1 + if processed_count % 100 == 0 or processed_count == total_new_images: + percentage = (processed_count / total_new_images) * 100 + print(f"Processed {processed_count} of {total_new_images} images ({percentage:.2f}%)...", flush=True) + except Exception as e: + print(f"Could not process image {full_path}: {e}") + + if images_to_add: + cursor.executemany(''' + INSERT INTO image_metadata (path, resolution, name, orientation, discovered_at) + VALUES (?, ?, ?, ?, ?) + ''', images_to_add) + conn.commit() + print(f"Successfully added {len(images_to_add)} new images to the database.") + + conn.close() # Update a selection in the database def update_selection(selection_id, action): @@ -197,6 +227,8 @@ def reset_database(): print(f"DEBUG: Reset database - deleted {rows_affected} selections") return rows_affected + + class ImageSwipeHandler(BaseHTTPRequestHandler): # Set response headers for CORS def _set_cors_headers(self): @@ -233,6 +265,34 @@ class ImageSwipeHandler(BaseHTTPRequestHandler): self.serve_file(path) except: self.send_error(404, "File not found") + + def do_POST(self): + parsed_url = urllib.parse.urlparse(self.path) + path = parsed_url.path + + if path == '/selection': + try: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + data = json.loads(post_data) + + add_selection(data['image_path'], data['action']) + + self.send_response(200) + self.send_header('Content-type', 'application/json') + self._set_cors_headers() + self.end_headers() + self.wfile.write(json.dumps({'status': 'success'}).encode()) + except Exception as e: + print(f"ERROR in do_POST /selection: {e}") + self.send_error(500, f"Server error processing selection: {e}") + else: + self.send_error(404, "Endpoint not found") + + def do_OPTIONS(self): + self.send_response(204) + self._set_cors_headers() + self.end_headers() def serve_file(self, file_path, content_type=None): try: @@ -283,75 +343,61 @@ class ImageSwipeHandler(BaseHTTPRequestHandler): self.send_error(404, f"Image not found: {image_path}") def serve_random_image(self): - print("DEBUG: serve_random_image() called") try: - # Get list of already actioned images - actioned_images = get_actioned_images() - print(f"DEBUG: Found {len(actioned_images)} already actioned images") - - # Get all resolution directories - resolutions = [d for d in os.listdir(IMAGE_DIR) if os.path.isdir(os.path.join(IMAGE_DIR, d))] - - # Try to find an unactioned image - max_attempts = 20 # Limit the number of attempts to find an unactioned image - for attempt in range(max_attempts): - # Choose a random resolution - resolution = random.choice(resolutions) - resolution_dir = os.path.join(IMAGE_DIR, resolution) - - # Get all images in the selected resolution directory - images = [f for f in os.listdir(resolution_dir) if f.endswith(('.png', '.jpg', '.jpeg'))] - - if not images: - continue # Try another resolution if this one has no images - - # Filter out already actioned images - unactioned_images = [img for img in images if f"{resolution}/{img}" not in actioned_images] - - # If we have unactioned images, choose one randomly - if unactioned_images: - image_name = random.choice(unactioned_images) - print(f"DEBUG: Found unactioned image: {resolution}/{image_name}") - break - elif attempt == max_attempts - 1: - # If we've tried max_attempts times and still haven't found an unactioned image, - # just choose any image - image_name = random.choice(images) - print(f"DEBUG: No unactioned images found after {max_attempts} attempts, using: {resolution}/{image_name}") - else: - # This will only execute if the for loop completes without a break - # Choose any random image as fallback - resolution = random.choice(resolutions) - resolution_dir = os.path.join(IMAGE_DIR, resolution) - images = [f for f in os.listdir(resolution_dir) if f.endswith(('.png', '.jpg', '.jpeg'))] - if not images: - self.send_error(404, "No images found in any resolution directory") - return - image_name = random.choice(images) - print(f"DEBUG: Using fallback random image: {resolution}/{image_name}") - - image_path = f"{resolution}/{image_name}" + parsed_url = urllib.parse.urlparse(self.path) + query_params = urllib.parse.parse_qs(parsed_url.query) + orientation_filter = query_params.get('orientation', ['all'])[0] + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Base query to get unactioned images + query = """ + SELECT meta.path, meta.resolution, meta.name, meta.orientation + FROM image_metadata meta + LEFT JOIN image_selections sel ON meta.path = sel.image_path + WHERE sel.image_path IS NULL + """ + + # Add orientation filter if specified + params = () + if orientation_filter != 'all': + query += " AND meta.orientation = ?" + params = (orientation_filter,) + + cursor.execute(query, params) + possible_images = cursor.fetchall() + conn.close() + + if not possible_images: + print("DEBUG: No matching unactioned images found.") + response = {'message': 'No more images available for this filter.'} + self.send_response(200) + self.send_header('Content-type', 'application/json') + self._set_cors_headers() + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + return + + # Choose one random image from the filtered list + chosen_image_row = random.choice(possible_images) + image_path = chosen_image_row[0] + resolution = chosen_image_row[1] + image_name = chosen_image_row[2] + orientation = chosen_image_row[3] full_image_path = os.path.join(IMAGE_DIR, image_path) - # Get the file creation time + print(f"DEBUG: Serving image: {image_path}") + + # Get file metadata try: file_stat = os.stat(full_image_path) - creation_time = file_stat.st_mtime # Use modification time as creation time + creation_time = file_stat.st_mtime creation_date = datetime.datetime.fromtimestamp(creation_time).strftime('%Y-%m-%d %H:%M:%S') except Exception as e: print(f"DEBUG ERROR getting file creation time: {str(e)}") creation_date = "Unknown" - # Determine if image is portrait or landscape - try: - from PIL import Image - with Image.open(full_image_path) as img: - width, height = img.size - orientation = "portrait" if height > width else "landscape" if width > height else "square" - except Exception as e: - print(f"DEBUG ERROR determining image orientation: {str(e)}") - orientation = "unknown" - # Return the image path as JSON response = { 'path': f"/images/{image_path}", @@ -367,6 +413,7 @@ class ImageSwipeHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(json.dumps(response).encode()) except Exception as e: + print(f"FATAL ERROR in serve_random_image: {e}") self.send_error(500, f"Error serving random image: {str(e)}") def serve_resolutions(self): @@ -627,7 +674,14 @@ class ImageSwipeHandler(BaseHTTPRequestHandler): def run(server_class=HTTPServer, handler_class=ImageSwipeHandler, port=8000): # Initialize the database init_db() - + + # Ensure the 'images' directory exists + if not os.path.exists(IMAGE_DIR): + os.makedirs(IMAGE_DIR) + + # Sync the image database on startup + sync_image_database() + server_address = ('', port) httpd = server_class(server_address, handler_class) print(f"Starting server on port {port}...") diff --git a/components/button.jsx b/components/button.jsx new file mode 100644 index 0000000..647dea1 --- /dev/null +++ b/components/button.jsx @@ -0,0 +1 @@ +// This file will contain the button component. diff --git a/history.html b/history.html index eeb24df..9e52356 100644 --- a/history.html +++ b/history.html @@ -5,325 +5,43 @@ Image Selection History - + -