Vinny Image System¶
Artist images are a first-class feature of Vinny. Every scraped event resolves to an artist photo, which is downloaded locally in one or more size variants and optionally synced to Cloudflare R2 for public CDN access.
Architecture Overview¶
flowchart TD
A["Scrape (LIV / XS / TAO)"] --> B["event.images\n(list of ImageMetadata)"]
B --> C["ImagePlugin.process_run()"]
C --> D["VEAImageUrl.from_url()\nParse VEA CDN URL"]
C --> E["Build size variant URLs\nper preset"]
C --> F["ImageDownloader\nasync, rate-limited, retryable"]
F --> G["data/images/artists/\n{artist-slug}_{size}.jpg"]
G -->|optional| H["R2Storage.upload()"]
H --> I["img.vinny.vegas/artists/\n{artist-slug}_{size}.jpg"]
style A fill:#7c3aed,color:#fff
style G fill:#059669,color:#fff
style I fill:#d97706,color:#fff
Key principle: Images are keyed by artist, not by event. dom-dolla_main.jpg
is downloaded once and reused across every Dom Dolla event, regardless of venue or
date. This means:
- No duplicate downloads for repeat performers
- Consistent file naming across all runs
- Fast re-scrapes — already-downloaded images are skipped automatically
Global Artists Directory¶
All images live in a single shared directory:
data/images/artists/
├── dom-dolla_small.jpg
├── dom-dolla_main.jpg
├── dom-dolla_large.jpg
├── cloonee_main.jpg
├── fisher_main.jpg
└── ...
Filename format: {artist-slug}_{size-preset}.jpg
The artist slug is derived from the performer name:
- "Dom Dolla" → dom-dolla
- "DJ Snake" → dj-snake
- "deadmau5" → deadmau5
- Special characters and spaces become hyphens; max 80 chars.
Size Presets¶
| Preset | Pixels | File Size | Use Case |
|---|---|---|---|
small |
250px | ~19 KB | Thumbnails, lists |
main |
500px | ~41 KB | Default — cards, web display |
medium |
750px | ~95 KB | Medium-quality display |
large |
1000px | ~133 KB | Large display |
hd |
1500px | ~290 KB | High-res / print |
raw |
orig | varies | Original unresized file |
Default: main (500px). Specify multiples with --size main --size small.
Migration note: Older serialized JSON may contain
"thumbnail"(→small) keys — these are silently migrated on load.
VEA CDN Integration¶
VEA CDN URL Structure
Both LIV and XS source images from VenueEventArtist.com (VEA), a CDN that supports on-the-fly resizing. Vinny can re-generate any size from any captured VEA URL, even if the scraper only found a 500px version.
Both LIV and XS source images from VenueEventArtist.com (VEA), a CDN that supports on-the-fly resizing via URL pattern:
Vinny uses VEAImageUrl to parse any VEA URL (regardless of baked-in size)
and re-generate URLs for any target size. This means you can download an
artist image at any size even if the scraper only captured a 500px URL.
See vea-image-transformations.md for full transformation reference (effects, crop modes, size benchmarks).
Data Model¶
ImageMetadata¶
Attached to each VegasEvent.images list. Represents one image source.
class ImageMetadata:
source_url: str # Original VEA CDN URL (any size)
category: str # "artist_full" | "artist_thumbnail" | etc.
sizes: dict[SizePreset, ImageSize] # Populated after download
status: ImageStatus # PENDING | DOWNLOADING | DOWNLOADED | FAILED | INVALID
ImageSize¶
One size variant within an ImageMetadata.
class ImageSize:
preset: SizePreset # "small" | "main" | "medium" | "large" | "hd" | "raw"
local_path: Path | None # Absolute path on disk after download
file_size: int | None # Bytes
mime_type: str | None # "image/jpeg"
downloaded_at: datetime | None
error_message: str | None
r2_url: str | None # Public R2 URL after upload (optional)
CLI Reference¶
Download images¶
# Download main (500px) images for the latest run
vinny images download
# Download multiple sizes
vinny images download --size main --size small --size hd
# Download and upload to R2 in one step
vinny images download --upload-r2
# Download for a specific run
vinny images download --run 2026-03-01-150542
# Adjust concurrency and rate limiting
vinny images download --workers 5 --delay 0.5 --retries 5
Sync to Cloudflare R2¶
# Sync ALL local artist images to R2 (recommended workflow)
vinny images upload-r2
# Upload + write R2 URLs back to D1 images table
vinny images upload-r2 --sync-d1
# Sync only specific sizes
vinny images upload-r2 --size main --size small
upload-r2 scans the entire data/images/artists/ directory, HEAD-checks each
file against R2, and uploads only what's missing. It is idempotent — safe to
run repeatedly.
--sync-d1 additionally writes each uploaded file's public R2 URL back to the
D1 images table (r2_url column), so any subsequent vinny export-d1 or
Markdown export will include R2 URLs instead of local paths. This is step 3 of
vinny sync and happens automatically when you use that command.
Retry failed downloads¶
Validate images¶
Verifies that downloaded files are valid JPEGs (not error pages, not truncated):
Status and stats¶
vinny images status # Status for latest run
vinny images status --run 2026-03-01-150542
vinny images stats # Aggregate across last 10 runs
Cloudflare R2 Integration¶
Images are stored in the vinny-vegas-images R2 bucket and served via the
public dev URL.
Required environment variables¶
R2_ACCOUNT_ID=<cloudflare-account-id>
R2_ACCESS_KEY_ID=<r2-api-key-id>
R2_SECRET_ACCESS_KEY=<r2-api-key-secret>
R2_PUBLIC_URL=https://img.vinny.vegas # custom domain on R2 bucket
R2_BUCKET=vinny-vegas-images # optional, this is the default
Set these in .env at the project root (loaded automatically via python-dotenv
or your shell's source).
R2 object key format¶
When an image was downloaded for a specific venue, the filename includes a venue suffix between the artist slug and the size preset:
artists/{artist-slug}_{venue-slug}_{size-preset}.jpg ← venue-specific
artists/{artist-slug}_{size-preset}.jpg ← venue-agnostic (legacy)
Venue suffix mapping:
| Venue | Slug |
|---|---|
| Encore Beach Club | ebc |
| Encore Beach Club At Night | ebcn |
| XS Nightclub | xs |
| LIV Las Vegas / LIV Nightclub | liv |
| LIV Beach | livb |
Examples:
artists/calvin-harris_ebc_main.jpg ← EBC-specific (500px)
artists/calvin-harris_ebc_hd.jpg ← EBC-specific (1500px)
artists/dom-dolla_main.jpg ← venue-agnostic (legacy LIV)
artists/cloonee_xs_main.jpg ← XS-specific
D1 event image columns¶
The events table has two image URL columns:
| Column | Content |
|---|---|
artist_image_url |
_main preset (500px) — used for cards and lists |
artist_image_url_full |
_hd preset (1500px) if available, else _main |
These are only populated from R2 URLs — they will be NULL until images are
uploaded and synced. vinny images sync-d1 reads from the run's image manifest
(the sizes field in events.json) and cannot backfill events where images
were uploaded outside the normal pipeline with an empty sizes dict. In that
case, generate a targeted UPDATE script from the local data/artists/ directory.
R2 Domain Migration
The old public URL (pub-xxx.r2.dev) has been replaced with a custom domain: img.vinny.vegas. Ensure R2_PUBLIC_URL in .env points to https://img.vinny.vegas. See scripts/migrate-r2-domain.sql if you need to rewrite existing D1 URLs.
Public URL format¶
This URL is stored in ImageSize.r2_url and exported to the D1 images table
as r2_url TEXT.
One-time R2 setup¶
-
Create bucket via Cloudflare MCP or dashboard:
POST /accounts/{id}/r2/buckets→{"name": "vinny-vegas-images", "locationHint": "wnam"} -
Add custom domain → R2 bucket settings → Custom Domains → add
img.vinny.vegas -
Create API token → Cloudflare Dashboard → R2 → Manage R2 API Tokens (no REST API endpoint for this — must be done manually)
-
Add to
.env(see above)
How Venues Feed the Pipeline¶
LIV Las Vegas / LIV Beach¶
- Extractor reads
artist_image_url_fullfrom the event page - Creates
ImageMetadata(source_url=..., category="artist_full") - VEA URL — multi-size download works natively
XS Nightclub (Wynn)¶
- Extractor reads image URL from Schema.org JSON-LD (
event["image"]) - Creates
ImageMetadata(source_url=..., category="artist_full") - Also a VEA CDN URL — same pipeline as LIV, no special handling
Both venues produce identical ImageMetadata objects. The image plugin doesn't
know or care which venue the event came from.
Typical Workflow¶
Recommended: one command¶
# Scrape + download images + R2 upload + D1 export
vinny sync
# Specific venue
vinny sync xs
vinny sync liv --with-pricing
# Skip the scrape step (resume from existing run)
vinny sync --run latest
vinny sync runs all four steps automatically. R2 URLs are written back to D1
as part of step 3, so the final D1 export always has r2_url populated.
Manual step-by-step¶
If you need finer control or want to run steps individually:
# 1. Scrape a venue
vinny scrape xs
# 2. Download images (main size by default)
vinny images download
# 3a. Sync everything to R2 (idempotent — skips already-uploaded files)
vinny images upload-r2
# 3b. Optionally write R2 URLs back to D1 at the same time
vinny images upload-r2 --sync-d1
# 4. Validate what was downloaded
vinny images validate
# 5. Export to D1 (r2_url column populated after step 3b)
vinny export-d1 --execute
Future Roadmap¶
- WebP conversion — Convert JPEGs to WebP before R2 upload for ~30% size savings
- ~~Custom domain on R2~~ — Done:
img.vinny.vegas(March 2026) - Srcset generation — Auto-generate
small/main/hdvariants for responsive web - Missing image backfill — Detect artists in master DB with no local image and download
- Image freshness — Re-download if VEA URL changes (new promo photo for same artist)
- Notion embed — Attach R2 URLs as cover images on Notion event pages
- Face detection crop — Use
KC(crop center) mode for consistent headshots
Last updated: 2026-03-03