Odysseus v1.0
This commit is contained in:
165
scripts/odysseus-gallery
Executable file
165
scripts/odysseus-gallery
Executable file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""odysseus-gallery — shell wrapper for the photo / image gallery.
|
||||
|
||||
Read-only by default (upload + edit operations need multipart payloads
|
||||
that are awkward at the shell). Filters images by tag, prompt, favorite,
|
||||
album, and surfaces EXIF metadata.
|
||||
|
||||
odysseus-gallery list --limit 20 --favorites
|
||||
odysseus-gallery show IMAGE_ID
|
||||
odysseus-gallery albums | jq -r '.[] | .name'
|
||||
odysseus-gallery search "sunset" # matches prompt + tags
|
||||
odysseus-gallery delete IMAGE_ID # soft-delete (is_active=False)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
||||
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
||||
quiet_logs()
|
||||
|
||||
import argparse, json, logging, os, sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from core.database import SessionLocal, GalleryImage, GalleryAlbum
|
||||
quiet_logs()
|
||||
except ModuleNotFoundError as e:
|
||||
sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def _serialize_image(i: "GalleryImage") -> dict:
|
||||
return {
|
||||
"id": i.id,
|
||||
"filename": i.filename,
|
||||
"prompt": (i.prompt or "")[:200],
|
||||
"model": i.model or "",
|
||||
"size": i.size or "",
|
||||
"tags": i.tags or "",
|
||||
"favorite": bool(i.favorite),
|
||||
"album_id": i.album_id or "",
|
||||
"session_id": i.session_id or "",
|
||||
"width": i.width,
|
||||
"height": i.height,
|
||||
"file_size": i.file_size,
|
||||
"taken_at": i.taken_at.isoformat() if i.taken_at else "",
|
||||
"camera_make": i.camera_make or "",
|
||||
"camera_model": i.camera_model or "",
|
||||
"created_at": i.created_at.isoformat() if i.created_at else "",
|
||||
}
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
q = db.query(GalleryImage).filter(GalleryImage.is_active == True) # noqa: E712
|
||||
if args.favorites:
|
||||
q = q.filter(GalleryImage.favorite == True) # noqa: E712
|
||||
if args.album:
|
||||
al = db.query(GalleryAlbum).filter(GalleryAlbum.name == args.album).first()
|
||||
if not al:
|
||||
fail(f"no album named {args.album!r}")
|
||||
q = q.filter(GalleryImage.album_id == al.id)
|
||||
if args.tag:
|
||||
q = q.filter(GalleryImage.tags.ilike(f"%{args.tag}%"))
|
||||
q = q.order_by(GalleryImage.created_at.desc()).limit(args.limit)
|
||||
emit([_serialize_image(i) for i in q.all()], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_show(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
i = db.get(GalleryImage, args.id)
|
||||
if not i:
|
||||
fail(f"no image with id {args.id!r}")
|
||||
out = _serialize_image(i)
|
||||
out["prompt_full"] = i.prompt or ""
|
||||
out["ai_tags"] = i.ai_tags or ""
|
||||
out["gps_lat"] = i.gps_lat or ""
|
||||
out["gps_lng"] = i.gps_lng or ""
|
||||
out["file_hash"] = i.file_hash or ""
|
||||
emit(out, args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_albums(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = db.query(GalleryAlbum).order_by(GalleryAlbum.name.asc()).all()
|
||||
emit([
|
||||
{"id": a.id, "name": a.name, "image_count": len(a.images)}
|
||||
for a in rows
|
||||
], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_search(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
like = f"%{args.query}%"
|
||||
rows = db.query(GalleryImage).filter(
|
||||
GalleryImage.is_active == True, # noqa: E712
|
||||
).filter(
|
||||
(GalleryImage.prompt.ilike(like)) |
|
||||
(GalleryImage.tags.ilike(like)) |
|
||||
(GalleryImage.ai_tags.ilike(like))
|
||||
).order_by(GalleryImage.created_at.desc()).limit(args.limit).all()
|
||||
emit([_serialize_image(i) for i in rows], args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
i = db.get(GalleryImage, args.id)
|
||||
if not i:
|
||||
fail(f"no image with id {args.id!r}")
|
||||
i.is_active = False
|
||||
db.commit()
|
||||
emit({"ok": True, "id": i.id, "soft_deleted": True}, args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--pretty", action="store_true")
|
||||
|
||||
p = argparse.ArgumentParser(prog="odysseus-gallery", parents=[common])
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pl = sub.add_parser("list", parents=[common])
|
||||
pl.add_argument("--limit", type=int, default=50)
|
||||
pl.add_argument("--favorites", action="store_true")
|
||||
pl.add_argument("--album", help="filter by album name")
|
||||
pl.add_argument("--tag", help="substring match against tags column")
|
||||
pl.set_defaults(func=cmd_list)
|
||||
|
||||
psh = sub.add_parser("show", parents=[common])
|
||||
psh.add_argument("id")
|
||||
psh.set_defaults(func=cmd_show)
|
||||
|
||||
pa = sub.add_parser("albums", parents=[common])
|
||||
pa.set_defaults(func=cmd_albums)
|
||||
|
||||
ps = sub.add_parser("search", parents=[common])
|
||||
ps.add_argument("query")
|
||||
ps.add_argument("--limit", type=int, default=50)
|
||||
ps.set_defaults(func=cmd_search)
|
||||
|
||||
pd = sub.add_parser("delete", parents=[common])
|
||||
pd.add_argument("id")
|
||||
pd.set_defaults(func=cmd_delete)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run(_build_parser()))
|
||||
Reference in New Issue
Block a user