190 lines
6.6 KiB
Python
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()
|