diff --git a/scripts/odysseus-gallery b/scripts/odysseus-gallery index ab892d798..20e210f04 100755 --- a/scripts/odysseus-gallery +++ b/scripts/odysseus-gallery @@ -6,6 +6,7 @@ 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 list --tag "sunset,beach" # all tags, manual or AI odysseus-gallery show IMAGE_ID odysseus-gallery albums | jq -r '.[] | .name' odysseus-gallery search "sunset" # matches prompt + tags @@ -83,7 +84,13 @@ def cmd_list(args): 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}%")) + for tag in (value.strip() for value in args.tag.split(",")): + if not tag: + continue + q = q.filter( + GalleryImage.tags.ilike(f"%{tag}%") | + GalleryImage.ai_tags.ilike(f"%{tag}%") + ) q = q.order_by(GalleryImage.created_at.desc()).limit(args.limit) emit([_serialize_image(i) for i in q.all()], args) finally: @@ -159,7 +166,7 @@ def _build_parser(): 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.add_argument("--tag", help="comma-separated tags; each matches manual or AI tags by substring") pl.set_defaults(func=cmd_list) psh = sub.add_parser("show", parents=[common]) diff --git a/tests/cli/test_gallery_cli_tag_filters.py b/tests/cli/test_gallery_cli_tag_filters.py new file mode 100644 index 000000000..b740b3488 --- /dev/null +++ b/tests/cli/test_gallery_cli_tag_filters.py @@ -0,0 +1,67 @@ +"""Gallery CLI tag filters match both sources and stack like the web library.""" +from datetime import datetime, timedelta + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from core.database import Base, GalleryAlbum, GalleryImage +from tests.helpers.cli_loader import load_script + + +@pytest.fixture +def gallery_cli(monkeypatch, tmp_path): + engine = create_engine(f"sqlite:///{tmp_path / 'gallery.db'}") + Base.metadata.create_all(engine) + sessions = sessionmaker(bind=engine) + with sessions() as db: + db.add(GalleryAlbum(id='sea', name='Sea')) + for index, (uid, tags, ai_tags, favorite, album) in enumerate([ + ('manual', 'sunset,beach', '', True, 'sea'), + ('ai', '', 'Beach,sunset', False, 'sea'), + ('split', 'sunset', 'beach', True, None), + ('one', 'sunset', '', False, None), + ('other', 'forest', 'moss', False, None), + ('inactive', 'sunset,beach', 'sunset,beach', True, 'sea'), + ]): + db.add(GalleryImage(id=uid, filename=uid+'.png', tags=tags, ai_tags=ai_tags, + favorite=favorite, album_id=album, is_active=uid!='inactive', + created_at=datetime(2026, 1, 1) + timedelta(minutes=index))) + db.commit() + cli = load_script('odysseus-gallery') + monkeypatch.setattr(cli, 'SessionLocal', sessions) + output = [] + monkeypatch.setattr(cli, 'emit', lambda rows, args: output.extend(rows)) + + def run(*argv): + output.clear() + args = cli._build_parser().parse_args(['list', *argv]) + args.func(args) + return [row['id'] for row in output] + + yield run + engine.dispose() + + +@pytest.mark.parametrize('tag,expected', [ + ('sunset', ['one', 'split', 'ai', 'manual']), + ('beach,sunset', ['split', 'ai', 'manual']), + ('sunset,beach', ['split', 'ai', 'manual']), + (', sunset, , BEACH ,', ['split', 'ai', 'manual']), + ('forest', ['other']), + ('no-match', []), + (' , ', ['other', 'one', 'split', 'ai', 'manual']), + ('sun', ['one', 'split', 'ai', 'manual']), +]) +def test_tag_sources_and_stacking(gallery_cli, tag, expected): + assert gallery_cli('--tag', tag) == expected + + +@pytest.mark.parametrize('options,expected', [ + ([], ['other', 'one', 'split', 'ai', 'manual']), + (['--tag', 'beach,sunset', '--favorites'], ['split', 'manual']), + (['--tag', 'beach,sunset', '--album', 'Sea'], ['ai', 'manual']), + (['--tag', 'beach', '--limit', '1'], ['split']), +]) +def test_tag_filters_preserve_other_list_options(gallery_cli, options, expected): + assert gallery_cli(*options) == expected