231 lines
8.2 KiB
Python
231 lines
8.2 KiB
Python
"""
|
||
preview_sample.py — zobrazí náhled fotek podle ID z databáze.
|
||
|
||
Použití:
|
||
python preview_sample.py 101 202 303 ...
|
||
python preview_sample.py --file ids.txt # jedno ID na řádek
|
||
python preview_sample.py --file ids.txt --thumb 400
|
||
"""
|
||
|
||
import argparse
|
||
import base64
|
||
import io
|
||
import sys
|
||
import tempfile
|
||
import webbrowser
|
||
from pathlib import Path
|
||
|
||
import psycopg2
|
||
from PIL import Image
|
||
|
||
# ── DB ────────────────────────────────────────────────────────────────────────
|
||
|
||
DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka",
|
||
password="Vlado7309208104++", database="fotky_buzalkovi")
|
||
|
||
# ── Cesty ─────────────────────────────────────────────────────────────────────
|
||
|
||
NAS_LINUX_PREFIX = "/mnt/user/ZalohaVsechObrazku/"
|
||
NAS_WIN_UNC = r"\\Tower1\ZalohaVsechObrazku\\"
|
||
|
||
|
||
def linux_to_win(p: str) -> Path:
|
||
rel = p.removeprefix(NAS_LINUX_PREFIX).replace("/", "\\")
|
||
return Path(NAS_WIN_UNC + rel)
|
||
|
||
|
||
# ── Načtení dat z DB ──────────────────────────────────────────────────────────
|
||
|
||
QUERY = """
|
||
SELECT
|
||
z.id,
|
||
z.cesta_zalohy,
|
||
z.velikost,
|
||
p.taken_at,
|
||
p.taken_at_source,
|
||
p.camera_make,
|
||
p.camera_model,
|
||
p.width,
|
||
p.height,
|
||
p.megapixels,
|
||
p.gps_lat,
|
||
p.gps_lon,
|
||
COALESCE(
|
||
(SELECT string_agg(e.error_code, ', ')
|
||
FROM photo_errors e WHERE e.photo_id = p.id),
|
||
''
|
||
) AS chyby
|
||
FROM zaloha_obrazku z
|
||
JOIN photos p ON p.zaloha_id = z.id
|
||
WHERE z.id = ANY(%s)
|
||
"""
|
||
|
||
|
||
def fetch_rows(ids: list[int]) -> list:
|
||
conn = psycopg2.connect(**DB)
|
||
cur = conn.cursor()
|
||
cur.execute(QUERY, (ids,))
|
||
rows = cur.fetchall()
|
||
conn.close()
|
||
# zachovej původní pořadí ID
|
||
order = {rid: i for i, rid in enumerate(ids)}
|
||
return sorted(rows, key=lambda r: order.get(r[0], 9999))
|
||
|
||
|
||
# ── Thumbnail ─────────────────────────────────────────────────────────────────
|
||
|
||
def make_thumb_b64(win_path: Path, size: int) -> str | None:
|
||
try:
|
||
img = Image.open(win_path)
|
||
img.thumbnail((size, size), Image.LANCZOS)
|
||
if img.mode not in ("RGB", "L"):
|
||
img = img.convert("RGB")
|
||
buf = io.BytesIO()
|
||
img.save(buf, format="JPEG", quality=82)
|
||
return base64.b64encode(buf.getvalue()).decode()
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
# ── HTML ──────────────────────────────────────────────────────────────────────
|
||
|
||
def format_size(n) -> str:
|
||
if n is None:
|
||
return "?"
|
||
for unit in ("B", "kB", "MB", "GB"):
|
||
if n < 1024:
|
||
return f"{n:.0f} {unit}"
|
||
n /= 1024
|
||
return f"{n:.1f} GB"
|
||
|
||
|
||
def render_html(rows: list, ids: list[int], thumb_size: int) -> str:
|
||
cards = []
|
||
ok = 0
|
||
|
||
for i, row in enumerate(rows, 1):
|
||
(rid, cesta, velikost, taken_at, taken_src,
|
||
make_, model, width, height, mp, lat, lon, chyby) = row
|
||
|
||
win_path = linux_to_win(cesta)
|
||
b64 = make_thumb_b64(win_path, thumb_size)
|
||
|
||
if b64:
|
||
ok += 1
|
||
img_tag = f'<img src="data:image/jpeg;base64,{b64}" alt="foto">'
|
||
card_cls = "card"
|
||
else:
|
||
img_tag = '<div class="no-img">⚠ soubor nedostupný<br><small>zkontroluj přístup na Tower1</small></div>'
|
||
card_cls = "card broken"
|
||
|
||
camera = " ".join(filter(None, [make_, model])) or "<em>neznámá</em>"
|
||
date_str = taken_at.strftime("%Y-%m-%d %H:%M") if taken_at else "?"
|
||
res = f"{width}×{height}" if width and height else "?"
|
||
mp_str = f"{mp:.1f} Mpx" if mp else ""
|
||
gps_link = (
|
||
f'<a href="https://maps.google.com/?q={lat},{lon}" target="_blank">📍 {lat:.4f}, {lon:.4f}</a>'
|
||
if lat and lon else ""
|
||
)
|
||
chyby_html = f'<div class="err">{chyby}</div>' if chyby else ""
|
||
path_short = str(win_path).replace(NAS_WIN_UNC, "…\\")
|
||
|
||
cards.append(f"""
|
||
<div class="{card_cls}">
|
||
<div class="thumb">{img_tag}</div>
|
||
<div class="meta">
|
||
<div class="idx">id={rid} #{i}</div>
|
||
<div><strong>{date_str}</strong> <span class="src">({taken_src})</span></div>
|
||
<div>📷 {camera}</div>
|
||
<div>{res} {mp_str} {format_size(velikost)}</div>
|
||
{"<div>" + gps_link + "</div>" if gps_link else ""}
|
||
{chyby_html}
|
||
<div class="path" title="{win_path}">{path_short}</div>
|
||
</div>
|
||
</div>""")
|
||
|
||
missing_ids = set(ids) - {r[0] for r in rows}
|
||
missing_note = ""
|
||
if missing_ids:
|
||
missing_note = f'<div class="warn">ID nenalezena v DB: {sorted(missing_ids)}</div>'
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="cs">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Preview fotek ({len(rows)})</title>
|
||
<style>
|
||
body {{ font-family: sans-serif; background: #1a1a2e; color: #eee; margin: 0; padding: 16px; }}
|
||
h1 {{ color: #e0c97f; margin-bottom: 4px; }}
|
||
.info {{ color: #aaa; margin-bottom: 16px; font-size: .9em; }}
|
||
.warn {{ color: #f99; margin-bottom: 12px; }}
|
||
.grid {{ display: flex; flex-wrap: wrap; gap: 16px; }}
|
||
.card {{ background: #16213e; border-radius: 8px; overflow: hidden;
|
||
width: {thumb_size + 24}px; box-shadow: 0 2px 8px #0006; }}
|
||
.card.broken {{ opacity: .55; }}
|
||
.thumb {{ display:flex; align-items:center; justify-content:center;
|
||
background:#0f3460; min-height: 120px; }}
|
||
.thumb img {{ max-width:100%; max-height:{thumb_size}px; display:block; }}
|
||
.no-img {{ color:#f88; padding:20px; text-align:center; font-size:.85em; }}
|
||
.meta {{ padding: 8px 12px; font-size: .78em; line-height: 1.75; }}
|
||
.meta strong {{ font-size: 1em; }}
|
||
.idx {{ float:right; color:#666; font-size:.75em; }}
|
||
.src {{ color:#888; }}
|
||
.err {{ color:#f99; }}
|
||
.path {{ color:#555; word-break:break-all; margin-top:4px; font-size:.7em; }}
|
||
a {{ color:#7ec8e3; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Preview fotek</h1>
|
||
<div class="info">
|
||
Celkem ID: <strong>{len(ids)}</strong> ·
|
||
Načteno z DB: <strong>{len(rows)}</strong> ·
|
||
Obrázky dostupné: <strong>{ok}/{len(rows)}</strong>
|
||
</div>
|
||
{missing_note}
|
||
<div class="grid">
|
||
{"".join(cards)}
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
|
||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Preview fotek podle ID z DB")
|
||
parser.add_argument("ids", nargs="*", type=int, help="ID fotek (zaloha_obrazku.id)")
|
||
parser.add_argument("--file", "-f", help="Soubor s ID (jedno na řádek)")
|
||
parser.add_argument("--thumb", "-t", type=int, default=320, help="Velikost thumbnailů v px (default 320)")
|
||
args = parser.parse_args()
|
||
|
||
ids = list(args.ids)
|
||
|
||
if args.file:
|
||
with open(args.file) as fh:
|
||
for line in fh:
|
||
line = line.strip()
|
||
if line and line.isdigit():
|
||
ids.append(int(line))
|
||
|
||
if not ids:
|
||
print("Žádná ID. Zadej je jako argumenty nebo přes --file.", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
print(f"Načítám {len(ids)} fotek z DB…")
|
||
rows = fetch_rows(ids)
|
||
print(f"Generuji thumbnaily (--thumb {args.thumb}px)…")
|
||
html = render_html(rows, ids, args.thumb)
|
||
|
||
tmp = tempfile.NamedTemporaryFile(
|
||
suffix=".html", delete=False, mode="w", encoding="utf-8"
|
||
)
|
||
tmp.write(html)
|
||
tmp.close()
|
||
print(f"Otevírám: {tmp.name}")
|
||
webbrowser.open(f"file:///{tmp.name}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|