immich-compare/preview-tool/grid.py

190 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Grid view of Jan 28, 2024 images for quick date assignment."""
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
import json
import subprocess
import urllib.parse
LIBRARY = "/tank/immich/library/library/admin"
JAN2024 = f"{LIBRARY}/2024/01"
API_KEY = "GsWQUTR6EXlkKp1M82jDJ3KmzhM0fMAbbIbfHDyI"
API_URL = "http://localhost:2283/api"
def get_images_from_db(limit=500):
"""Get images from Immich database."""
result = subprocess.run([
"docker", "exec", "immich_postgres", "psql", "-U", "postgres", "-d", "immich", "-t", "-A", "-F", "|", "-c",
f"""SELECT id, "originalFileName", "originalPath"
FROM asset
WHERE DATE("fileCreatedAt") = '2024-01-28'
AND "deletedAt" IS NULL
AND "type" = 'IMAGE'
ORDER BY "originalFileName"
LIMIT {limit};"""
], capture_output=True, text=True)
images = []
for line in result.stdout.strip().split('\n'):
if '|' in line:
parts = line.split('|')
if len(parts) >= 3:
images.append({
'id': parts[0],
'filename': parts[1],
'path': parts[2].replace('/data/library/admin/', '')
})
return images
def generate_html(limit=20):
images = get_images_from_db(limit)
cards = []
for img in images:
cards.append(f'''
<div class="card" onclick="toggle(this)" data-id="{img['id']}" data-name="{img['filename']}">
<img src="/image/{img['path']}" loading="lazy">
<div class="name">{img['filename']}</div>
<div class="id">{img['id']}</div>
</div>''')
return f'''<!DOCTYPE html>
<html>
<head>
<title>Jan 28, 2024 - Grid View</title>
<style>
body {{ font-family: Arial; margin: 20px; background: #1a1a1a; color: #eee; }}
h1 {{ color: #4a9eff; }}
.grid {{ display: flex; flex-wrap: wrap; gap: 10px; }}
.card {{
background: #2a2a2a;
padding: 8px;
border-radius: 5px;
cursor: pointer;
border: 3px solid transparent;
width: 180px;
}}
.card:hover {{ border-color: #666; }}
.card.selected {{ border-color: #4a9; background: #234; }}
.card img {{ width: 100%; height: 140px; object-fit: cover; border-radius: 3px; }}
.name {{ font-size: 11px; margin-top: 5px; word-break: break-all; color: #aaa; }}
.id {{ font-size: 10px; color: #666; }}
.controls {{
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #333;
padding: 15px;
border-top: 2px solid #555;
}}
.output {{
background: #111;
padding: 10px;
font-family: monospace;
font-size: 12px;
margin-top: 10px;
min-height: 40px;
white-space: pre-wrap;
user-select: all;
}}
button {{ padding: 8px 15px; margin-right: 10px; cursor: pointer; }}
.count {{ color: #4a9; font-weight: bold; }}
</style>
</head>
<body>
<h1>Jan 28, 2024 Images ({len(images)} shown)</h1>
<p>Click images to select. Selected IDs appear below for copying.</p>
<div class="grid">
{''.join(cards)}
</div>
<div class="controls">
<button onclick="selectAll()">Select All</button>
<button onclick="clearAll()">Clear</button>
<button onclick="copyIds()">Copy IDs</button>
<button onclick="copyNames()">Copy Filenames</button>
<span class="count" id="count">0 selected</span>
<div class="output" id="output">Click images to select, then copy IDs or filenames</div>
</div>
<script>
function toggle(el) {{
el.classList.toggle('selected');
updateOutput();
}}
function selectAll() {{
document.querySelectorAll('.card').forEach(c => c.classList.add('selected'));
updateOutput();
}}
function clearAll() {{
document.querySelectorAll('.card').forEach(c => c.classList.remove('selected'));
updateOutput();
}}
function getSelected() {{
return Array.from(document.querySelectorAll('.card.selected'));
}}
function updateOutput() {{
const sel = getSelected();
document.getElementById('count').textContent = sel.length + ' selected';
const ids = sel.map(c => c.dataset.id).join('\\n');
document.getElementById('output').textContent = ids || 'Click images to select';
}}
function copyIds() {{
const ids = getSelected().map(c => c.dataset.id).join('\\n');
navigator.clipboard.writeText(ids);
alert('Copied ' + getSelected().length + ' IDs');
}}
function copyNames() {{
const names = getSelected().map(c => c.dataset.name).join('\\n');
navigator.clipboard.writeText(names);
alert('Copied ' + getSelected().length + ' filenames');
}}
</script>
</body>
</html>'''
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = urllib.parse.unquote(self.path)
if path == '/' or path.startswith('/?'):
# Parse limit from query string
limit = 500
if '?' in path:
params = urllib.parse.parse_qs(path.split('?')[1])
limit = int(params.get('limit', [500])[0])
html = generate_html(limit)
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(html.encode())
elif path.startswith('/image/'):
img_path = LIBRARY + "/" + path[7:]
if os.path.exists(img_path):
self.send_response(200)
ext = img_path.lower().split('.')[-1]
ctype = 'image/jpeg' if ext in ['jpg', 'jpeg'] else f'image/{ext}'
self.send_header('Content-type', ctype)
self.end_headers()
with open(img_path, 'rb') as f:
self.wfile.write(f.read())
else:
self.send_response(404)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
if __name__ == '__main__':
port = 5001
print(f"Grid view at http://localhost:{port}")
print(f"Add ?limit=100 for more images")
HTTPServer(('0.0.0.0', port), Handler).serve_forever()