Stock Art Collector

A CLI and web tool for collecting and managing stock art assets from multiple provider APIs. Features async downloads, rate limiting, duplicate detection, license verification, and attribution management. Graduated from staging to apps/ zone on 2026-01-29.

Overview

Stock Art Collector (SAC) aggregates stock media assets — photos, videos, vectors, audio — from Unsplash, Pexels, Pixabay, Freepik, and Coverr through a uniform adapter interface. Assets are stored in a local SQLite catalog with full metadata, quality thresholds, and license tracking. A FastAPI + HTMX web interface (port 3070) provides search and collection management alongside the Click CLI.

The project dogfoods the Source Adapter Interface pattern with a dynamic registry that auto-discovers adapters at runtime — no hardcoded provider lists.

Current phase: Phase 6 — UX Polish + Performance Tuning Maturity: L3 (Verified) — Multi-source E2E confirmed 2026-03-11 Test coverage: >85% — 214/215 tests passing (1 known flaky serve test)

Tech Stack

TechnologyVersionPurpose
Python3.11+Runtime — async support
SQLite / SQLAlchemy3.x / 2.0+Local catalog, async ORM
aiosqlite0.xAsync SQLite driver
Click8.xCLI framework
FastAPI0.109+Web interface (port 3070)
HTMX1.9+Server-driven frontend
httpx0.27+Async HTTP client for API calls
Pydantic2.xType-safe config with SecretStr
imagehash4.xPerceptual hashing for duplicate detection
Pillow10.xImage metadata / EXIF extraction
FFprobe—Video metadata (FFmpeg suite)
Mutagen1.47+Audio metadata — ID3, duration, bitrate
Alembic—Database migrations

Source adapters: Unsplash, Pexels, Pixabay, Freepik, Coverr

Key Decisions

DecisionRationaleOutcome
SQLite for local storageSimplicity, portability, no external dependenciesImplemented via aiosqlite
HTMX for web interfaceMinimal JS, server-driven interactionsPhase 7 — implemented
Click for CLI frameworkMature, well-documented, composableImplemented
Pydantic for configurationType-safe settings, env var supportImplemented (CB-SAC-001)
imagehash for duplicate detectionMultiple hash algorithms, pure PythonImplemented (CB-SAC-006)
Token bucket for rate limitingSimple, effective, per-source time windowsImplemented (CB-SAC-003)
Adapter pattern for sourcesUniform interface, easy to add providersImplemented — 5 adapters
Dynamic source registryCentralised auto-discovery, no hardcoded listsImplemented (SRC-01)
Streaming downloadshttpx stream + chunked aiofiles writes, prevents OOM on videoImplemented (DL-01)
SecretStr for API keysPrevents accidental logging of credentialsImplemented
Path traversal protectionPath.resolve() + is_relative_to() on all user-supplied pathsImplemented (Phase 8)

Circuit Breakers

6 circuit breakers guard production integrity:

IDTriggerPriorityResponse
CB-SAC-001Hardcoded API key detectedP1 CRITICALBlock commit, rotate key
CB-SAC-002Asset without verified licenseP1 CRITICALQuarantine, require manual review
CB-SAC-0033 consecutive 429 responsesP2 HIGH15-min cooldown, exponential backoff
CB-SAC-004CC-BY asset missing attributionP2 HIGHBlock export until attribution added
CB-SAC-005Asset below quality thresholdP3 MEDIUMWarning on import, optional override
CB-SAC-006>10% duplicates in batchP3 MEDIUMReview before import completes

Patterns Established

PatternDescriptionReusable
Source adapter interfaceAbstract base with search, get_asset, download, check_healthYes
Dynamic source registryAuto-discovery singleton, eliminates hardcoded source listsYes
Circuit breakerConfigurable thresholds, HTTP fault toleranceYes
Token bucket rate limitingPer-source windows (per-hour, per-minute)Yes
Async session context managerCommit/rollback, prevents lazy loading in async ORMYes
Perceptual hash similarityMulti-algorithm (2/3 required for match)Yes
SecretStr for credentialsPydantic pattern to prevent secret loggingYes
Path traversal protectionValidate user paths against allowed rootsYes

Lessons Learned

  • Empty SecretStr config bug: KEY= in .env becomes SecretStr(''), not None. Sources with empty keys falsely reported as configured and failed at runtime. Fix: _has_api_key() checks bool(value.get_secret_value()).
  • Pydantic-settings + tests: Config tests fail when .env exists because pydantic-settings loads it automatically. Use Settings(_env_file=None) for tests asserting default/unconfigured state.
  • Download-to-catalog gap: sac download saves file to disk but never inserts the Asset record. sac list --downloaded-only returns empty. Pipeline resolver (sac://) depends on catalog — persistence step needed.
  • async SQLAlchemy relationship loading: N+1 queries if not explicit. Always use selectinload/joinedload in async context.
  • FastAPI lifespan pattern: on_event("startup") is deprecated — use asynccontextmanager lifespan.
  • Click async wrapper: Click is sync-only. Async CLI commands require run_async() wrapper pattern.

Technical Debt

ItemPriorityNotes
Download-to-catalog gapHighsac download skips catalog insert — pipeline resolver broken
Flaky serve testMediumTestServeCommand blocks pytest — needs pytest-timeout or mock
Video metadata requires FFmpegMediumExternal dep, should degrade gracefully
No persistent rate limit stateLowBG-007 — state resets on restart
Export hardcodes limit=1000LowConfigurable via --limit flag added in Phase 1 tech debt

Status

Phase 6 — UX Polish + Performance Tuning (active as of 2026-03-11)

  • All 5 source adapters verified: Unsplash, Pexels, Pixabay, Freepik, Coverr
  • 214/215 tests passing; 1 known flaky serve test (BG-008)
  • Rate limit persistence pending (BG-007)
  • Web UI (port 3070): search results template optimization + asset detail view in progress
PhaseStatusKey Deliverables
0: ComplianceCompleteCI, Dependabot, pre-commit, concerns.md
1: Core InfrastructureCompleteConfig, models, DB session, migrations
2: Source AdaptersCompleteUnsplash, Pexels, Pixabay adapters
3: Metadata & OrganizationCompletePhoto/video/audio metadata, hashing, duplicates
4: CLI ImplementationCompleteAll commands functional
5: New Source AdaptersComplete (2026-03-11)Freepik, Coverr, parallel aggregation
6: UX PolishIn ProgressPort 3070 UI, asset detail, export UX