Skip to content

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:

https://venueeventartist.com/imateq/event/{venue}/{event}/{artist}/{SIZE}SC{SIZE}/{image_id}.jpeg

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

vinny images retry
vinny images retry --size hd --workers 5

Validate images

Verifies that downloaded files are valid JPEGs (not error pages, not truncated):

vinny images validate

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

https://img.vinny.vegas/artists/dom-dolla_main.jpg

This URL is stored in ImageSize.r2_url and exported to the D1 images table as r2_url TEXT.

One-time R2 setup

  1. Create bucket via Cloudflare MCP or dashboard: POST /accounts/{id}/r2/buckets{"name": "vinny-vegas-images", "locationHint": "wnam"}

  2. Add custom domain → R2 bucket settings → Custom Domains → add img.vinny.vegas

  3. Create API token → Cloudflare Dashboard → R2 → Manage R2 API Tokens (no REST API endpoint for this — must be done manually)

  4. Add to .env (see above)


How Venues Feed the Pipeline

LIV Las Vegas / LIV Beach

  • Extractor reads artist_image_url_full from 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

# 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/hd variants 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