library(artutils)
artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
# Avatar/thumbnail (art-public bucket)
avatar_url <- path_artist_thumb(artist)
cat("Avatar:", avatar_url, "\n")
# Returns: https://art-public.nyc3.cdn.digitaloceanspaces.com/artists/{uuid}/thumb.jpeg
# Path construction is pure - works in data.table operations
artists_dt <- data.table::data.table(
artist_uuid = c("uuid1", "uuid2", "uuid3"),
artist_name = c("Alice", "Bob", "Carol")
)
artists_dt[, avatar_url := path_artist_thumb(artist_uuid)]
print(artists_dt)Working with CDN Assets
This vignette covers artutils path functions for constructing CDN URLs and managing asset workflows. These functions are pure string operations - no database queries, no network calls - making them safe to use in loops and vectorized operations.
Understanding the CDN Architecture
Artalytics uses DigitalOcean Spaces (S3-compatible) for asset storage with three buckets:
art-public/ → Public thumbnails, site images (no auth required)
art-data/ → Processed assets (frames, graphs, variants)
art-vault/ → Original uploads, canvas files (restricted access)
Bucket Security Model
| Bucket | Access | Contents | URL Type |
|---|---|---|---|
art-public |
Public read | Thumbnails, avatars, certificates | Direct HTTPS |
art-data |
Private | Replay frames, analytics graphs | Direct HTTPS |
art-vault |
Private | Canvas files, original uploads | Presigned URLs |
All artutils path functions return direct HTTPS URLs for art-public and art-data. For art-vault assets, use artcore’s presigning functions.
Asset Types & Path Functions
Artist Assets
Artwork Thumbnails & Images
artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"
# Thumbnail for gallery grids (art-public)
thumb_url <- path_artwork_thumb(artist, artwork)
cat("Thumbnail:", thumb_url, "\n")
# Full-size main image (art-public)
main_url <- path_art_main_image(artist, artwork)
cat("Main image:", main_url, "\n")
# Original canvas file (art-vault - use presigned URL)
canvas_url <- path_art_canvas(artist, artwork)
cat("Canvas file:", canvas_url, "\n")
# This returns a path - actual download requires presigning via artcoreReplay Player Assets
The replay player displays a time-lapse sequence of frames and corresponding analytics graphs.
artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"
# Frame sequence (art-data bucket)
# Frames are numbered 1, 2, 3, ..., N
frame_1_url <- path_replay_frame(artist, artwork, which = 1)
frame_50_url <- path_replay_frame(artist, artwork, which = 50)
cat("Frame 1:", frame_1_url, "\n")
cat("Frame 50:", frame_50_url, "\n")
# Analytics graphs (art-data bucket)
graph_1_url <- path_replay_graph(artist, artwork, which = 1)
cat("Graph 1:", graph_1_url, "\n")
# Prefix for listing all frames (used with artcore::cdn_list_keys)
frames_prefix <- prefix_replay_frames(artist, artwork)
cat("Frames prefix:", frames_prefix, "\n")
# Returns: replays/{artist}/{artwork}/frames/Certificate Assets
Certificates of authenticity are issued as both PDF and JPEG.
artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"
cert_id <- "cert-20240115-abc123"
# PDF certificate for download
pdf_url <- path_artwork_cert(artist, artwork, cert_id, type = "pdf")
cat("Certificate PDF:", pdf_url, "\n")
# JPEG certificate for preview/display
jpeg_url <- path_artwork_cert(artist, artwork, cert_id, type = "jpeg")
cat("Certificate JPEG:", jpeg_url, "\n")
# Certificate thumbnail (smaller JPEG)
cert_thumb_url <- path_artwork_cert_thumb(artist, artwork, cert_id)
cat("Certificate thumb:", cert_thumb_url, "\n")
# Certificate frame asset (used in certificate generation)
frame_asset_url <- path_cert_frame_asset(artist, artwork, which = 1)
cat("Cert frame 1:", frame_asset_url, "\n")Gallery Variants
Gallery variants are alternative crops, formats, or styles of an artwork.
artist <- "746b8207-72f5-4ab6-8d19-a91d03daec3d"
artwork <- "99a61148-1d3b-4340-8cf6-92ad26046b0f"
# Specific variant file
variant_url <- path_gallery_asset(artist, artwork, "square-crop.jpeg")
cat("Square crop:", variant_url, "\n")
# Common variant patterns
portrait_url <- path_gallery_asset(artist, artwork, "portrait.jpeg")
landscape_url <- path_gallery_asset(artist, artwork, "landscape.jpeg")
social_url <- path_gallery_asset(artist, artwork, "social-1200x630.jpeg")
# Prefix for listing all variants
gallery_prefix <- prefix_gallery(artist, artwork)
cat("Gallery prefix:", gallery_prefix, "\n")Workflow 1: Building a Replay Player
The replay player needs frame images and corresponding analytics graphs for each frame.
build_replay_player_data <- function(artist_uuid, artwork_uuid) {
# Step 1: Get frame count from database
frames <- get_frame_analytics(artist_uuid, artwork_uuid)
n_frames <- nrow(frames)
if (n_frames == 0) {
return(list(error = "no_frames"))
}
# Step 2: Generate all frame URLs (vectorized)
frame_numbers <- seq_len(n_frames)
frame_urls <- sapply(frame_numbers, function(i) {
path_replay_frame(artist_uuid, artwork_uuid, which = i)
})
graph_urls <- sapply(frame_numbers, function(i) {
path_replay_graph(artist_uuid, artwork_uuid, which = i)
})
# Step 3: Combine with frame analytics
player_data <- data.table::data.table(
frame = frame_numbers,
elapsed_hours = frames$elapsed_hours,
cumulative_strokes = frames$cumulative_strokes,
unique_colors = frames$unique_colors,
estimated_bpm = frames$estimated_bpm,
technique_phase = frames$technique_phase,
frame_url = frame_urls,
graph_url = graph_urls
)
# Step 4: Calculate playback metadata
total_duration_secs <- max(frames$elapsed_hours) * 3600
playback_speed_options <- c(0.5, 1, 2, 4, 8) # Relative to real-time
list(
frames = player_data,
metadata = list(
n_frames = n_frames,
total_duration_hours = max(frames$elapsed_hours),
total_duration_secs = total_duration_secs,
avg_frame_duration = total_duration_secs / n_frames,
playback_speeds = playback_speed_options,
# Prefixes for batch operations
frames_prefix = prefix_replay_frames(artist_uuid, artwork_uuid),
graphs_prefix = prefix_replay_graphs(artist_uuid, artwork_uuid)
)
)
}
# Use it
player <- build_replay_player_data(
"746b8207-72f5-4ab6-8d19-a91d03daec3d",
"99a61148-1d3b-4340-8cf6-92ad26046b0f"
)
if (is.null(player$error)) {
cat("Replay player ready\n")
cat("Frames:", player$metadata$n_frames, "\n")
cat("Duration:", round(player$metadata$total_duration_hours, 2), "hours\n")
cat("First frame:", player$frames$frame_url[1], "\n")
cat("Last frame:", player$frames$frame_url[nrow(player$frames)], "\n")
}Workflow 2: Gallery Grid with Lazy Loading
Build a responsive gallery grid with lazy loading and multiple image sizes.
build_gallery_grid <- function(artist_uuid, limit = 20, offset = 0) {
cn <- artcore::dbc()
on.exit(artcore::dbd(cn))
# Step 1: Get artworks for grid
recent <- get_artist_recent_works(artist_uuid, limit = limit, cn = cn)
if (nrow(recent) == 0) {
return(list(items = list(), total = 0))
}
# Step 2: Enrich with multiple image sizes
# Vectorized path generation
recent[, `:=`(
# Thumbnail for initial grid load (small, fast)
thumb_url = path_artwork_thumb(artist_uuid, art_uuid),
# Main image for lightbox/detail view (large)
main_url = path_art_main_image(artist_uuid, art_uuid),
# Social sharing image (optimized dimensions)
social_url = path_gallery_asset(
artist_uuid, art_uuid, "social-1200x630.jpeg"
),
# Square crop for uniform grid
square_url = path_gallery_asset(
artist_uuid, art_uuid, "square-crop.jpeg"
)
)]
# Step 3: Build lazy loading metadata
recent[, `:=`(
# Preload thumbnail, lazy load others
loading_strategy = "lazy",
thumbnail_priority = ifelse(
seq_len(.N) <= 6, # First 6 items
"high",
"low"
)
)]
list(
items = recent,
total = nrow(recent),
grid_config = list(
columns = 3, # Responsive: 1 on mobile, 2 on tablet, 3 on desktop
gap = "1rem",
aspect_ratio = "1 / 1", # Square grid cells
lazy_threshold = 6 # Preload first 6, lazy load rest
)
)
}
# Use it in Shiny
output$gallery_grid <- renderUI({
grid <- build_gallery_grid(rv$artist_uuid, limit = 20)
# Map to HTML img tags
img_tags <- lapply(seq_len(nrow(grid$items)), function(i) {
item <- grid$items[i, ]
tags$div(
class = "grid-item",
tags$img(
src = item$thumb_url,
alt = item$art_title,
loading = if (i <= grid$grid_config$lazy_threshold) "eager" else "lazy",
`data-full-url` = item$main_url,
onclick = paste0("openLightbox('", item$art_uuid, "')")
),
tags$p(class = "caption", item$art_title)
)
})
tags$div(class = "gallery-grid", img_tags)
})Workflow 3: Certificate Download Handler
Provide certificate downloads with proper content types and filenames.
# In Shiny server
output$download_certificate <- downloadHandler(
filename = function() {
# Get artwork title for filename
appdata <- rv$appdata
title_slug <- stringr::str_replace_all(
tolower(appdata$artwork$info$basic$art_title),
"[^a-z0-9]+",
"-"
)
cert_id <- appdata$certificate$cert_id
paste0("certificate-", title_slug, "-", cert_id, ".pdf")
},
content = function(file) {
# Get certificate URL
cert_url <- path_artwork_cert(
rv$artist_uuid,
rv$artwork_uuid,
rv$appdata$certificate$cert_id,
type = "pdf"
)
# Download from CDN to temp file, then serve
download.file(cert_url, file, mode = "wb", quiet = TRUE)
},
contentType = "application/pdf"
)
# JPEG version for preview
output$certificate_preview <- renderImage({
req(rv$appdata$certificate$cert_id)
jpeg_url <- path_artwork_cert(
rv$artist_uuid,
rv$artwork_uuid,
rv$appdata$certificate$cert_id,
type = "jpeg"
)
# Shiny can render URLs directly
list(src = jpeg_url, contentType = "image/jpeg", alt = "Certificate preview")
}, deleteFile = FALSE)Workflow 4: Batch Asset Verification
Verify that expected assets exist on CDN before displaying UI elements.
verify_artwork_assets <- function(artist_uuid, artwork_uuid) {
# This requires artcore functions for actual existence checks
# artutils only provides path construction
required_assets <- list(
thumbnail = path_artwork_thumb(artist_uuid, artwork_uuid),
main_image = path_art_main_image(artist_uuid, artwork_uuid),
replay_prefix = prefix_replay_frames(artist_uuid, artwork_uuid)
)
verification_results <- list()
for (asset_name in names(required_assets)) {
asset_path <- required_assets[[asset_name]]
# Determine bucket and key from URL
if (grepl("art-public", asset_path)) {
bucket <- "art-public"
key <- sub(".*art-public.*/", "", asset_path)
exists <- artcore::has_object(bucket, key)
verification_results[[asset_name]] <- exists
} else if (grepl("art-data", asset_path)) {
bucket <- "art-data"
key <- sub(".*art-data.*/", "", asset_path)
# For prefixes, check if any objects exist
if (grepl("/$", key)) {
exists <- artcore::has_prefix(bucket, key)
} else {
exists <- artcore::has_object(bucket, key)
}
verification_results[[asset_name]] <- exists
}
}
# Determine what UI features can be enabled
list(
results = verification_results,
features_available = list(
can_show_thumbnail = verification_results$thumbnail,
can_show_main_image = verification_results$main_image,
can_enable_replay = verification_results$replay_prefix
)
)
}
# Use in Shiny reactive
artwork_features <- reactive({
req(rv$artist_uuid, rv$artwork_uuid)
verify_artwork_assets(rv$artist_uuid, rv$artwork_uuid)
})
# Conditionally render UI
output$replay_button <- renderUI({
if (artwork_features()$features_available$can_enable_replay) {
actionButton("show_replay", "Play Timelapse", icon = icon("play"))
} else {
tags$p("Replay not available for this artwork")
}
})Common Patterns & Best Practices
Pattern: Vectorized Path Generation
# GOOD - Vectorized (fast)
artworks_dt <- get_artist_recent_works(artist_uuid, limit = 50)
artworks_dt[, thumb_url := path_artwork_thumb(artist_uuid, art_uuid)]
artworks_dt[, main_url := path_art_main_image(artist_uuid, art_uuid)]
# AVOID - Loop (slow and verbose)
thumb_urls <- character(nrow(artworks_dt))
for (i in seq_len(nrow(artworks_dt))) {
thumb_urls[i] <- path_artwork_thumb(
artworks_dt$artist_uuid[i],
artworks_dt$art_uuid[i]
)
}Pattern: Conditional Asset Loading
# Load different assets based on viewport
load_artwork_image <- function(artist, artwork, viewport_width) {
if (viewport_width < 768) {
# Mobile - use thumbnail
path_artwork_thumb(artist, artwork)
} else if (viewport_width < 1200) {
# Tablet - use square crop
path_gallery_asset(artist, artwork, "square-crop.jpeg")
} else {
# Desktop - use main image
path_art_main_image(artist, artwork)
}
}Pattern: Fallback Images
get_avatar_with_fallback <- function(artist_uuid) {
primary <- path_artist_thumb(artist_uuid)
# Check if exists (requires artcore)
if (artcore::has_object("art-public", extract_key_from_url(primary))) {
return(primary)
}
# Fallback to default avatar
path_image_asset("default-avatar.jpeg")
}Performance Considerations
Path Functions Are Free
All artutils path functions are pure string operations with negligible cost:
# Benchmark: 1 million path constructions
system.time({
for (i in seq_len(1e6)) {
path_artwork_thumb("746b8207-72f5-4ab6-8d19-a91d03daec3d",
"99a61148-1d3b-4340-8cf6-92ad26046b0f")
}
})
# Typical result: ~0.5 seconds for 1M calls
# = 0.0000005 seconds per callCompare to database queries: - Database query: 5-50ms per call - Path function: 0.0005ms per call - 10,000x faster
Use Paths Liberally
Because path functions are so cheap, you can: - Generate paths in tight loops without concern - Add multiple path columns to data.tables freely - Recalculate paths on every render (no caching needed)
Next Steps
- Quickstart - Get started with basic queries
- Data Access Patterns - Query workflows
- Advanced Workflow - Complex scenarios
- Reference - All path functions documented
