diff --git a/Documentation/API.md b/Documentation/API.md index 156fab5..bbdf612 100644 --- a/Documentation/API.md +++ b/Documentation/API.md @@ -164,8 +164,25 @@ Painting detail with influence graph neighbours. ```json { "painting": { "id": 10, "title": "...", "artist_name": "...", "image_path": "...", "has_influence_links": true, ... }, - "influencedBy": [ { "id": 3, "title": "...", "artist_name": "...", "notes": "...", "aspects": "...", "quote": "...", "source_author": "...", ... } ], - "influenced": [ { "id": 20, "title": "...", ... } ] + "influencedBy": [ + { + "source_type": "painting", + "id": 3, "title": "...", "artist_name": "...", "year": 1885, + "notes": "...", "aspects": "...", "quote": "...", "source_author": "...", "source_url": "..." + }, + { + "source_type": "artist", + "source_artist_id": 12, "source_artist_name": "Paul Cézanne", + "artist_portrait": "...", "period_note": "during creation (1907)", + "period_start_year": 1907, "period_end_year": 1907 + }, + { + "source_type": "movement", + "movement_id": 8, "movement_name": "Fauvism", "movement_color": "#c45c26", + "period_start_year": 1905, "period_end_year": 1907 + } + ], + "influenced": [ { "source_type": "painting", "id": 20, "title": "...", ... } ] } ``` diff --git a/Documentation/DB_structure.md b/Documentation/DB_structure.md index 4458b81..ca56dcf 100644 --- a/Documentation/DB_structure.md +++ b/Documentation/DB_structure.md @@ -24,6 +24,9 @@ erDiagram artist_periods ||--o{ paintings : groups paintings ||--o{ painting_influences : influenced_by paintings ||--o{ painting_influences : influences + paintings ||--o{ painting_influence_sources : influenced_by + artists ||--o{ painting_influence_sources : artist_source + art_movements ||--o{ painting_influence_sources : movement_source ``` ## Tables @@ -114,7 +117,35 @@ Directed edges: *this painting* was influenced by *that painting*. Unique constraint on `(painting_id, influenced_by_painting_id)`. -Used by the 3D gallery exit panel: predecessors are artists of `influenced_by_painting_id` works; successors are artists of paintings influenced by this artist’s works (see [API.md](API.md#get-apiartistsidnavigation)). +Used by the 3D gallery exit panel: predecessors are artists of `influenced_by_painting_id` works; successors are artists of paintings influenced by this artist’s works (see [API.md](API.md#get-apiartistsidnavigation)). Legacy table; new polymorphic links live in `painting_influence_sources`. + +### `painting_influence_sources` + +Polymorphic influence links: *this painting* was influenced by a **painting**, **artist**, or **movement**. + +| Column | Type | Notes | +|--------|------|-------| +| `id` | SERIAL PK | | +| `painting_id` | FK → `paintings` | The work being explained | +| `source_type` | VARCHAR(20) | `painting`, `artist`, or `movement` | +| `source_painting_id` | FK → `paintings` | When `source_type = painting` | +| `source_artist_id` | FK → `artists` | When `source_type = artist` | +| `source_movement_id` | FK → `art_movements` | When `source_type = movement` | +| `period_note` | VARCHAR(240) | Human-readable period label | +| `period_start_year` | INTEGER | Optional span start | +| `period_end_year` | INTEGER | Optional span end | +| `notes` | TEXT | Curator summary | +| `source` | VARCHAR(500) | General attribution | +| `aspects` | TEXT | What was borrowed | +| `quote` | TEXT | Short citation | +| `source_author` | VARCHAR(200) | e.g. Gombrich, Janson | +| `source_url` | VARCHAR(500) | Reference link | +| `discovered_via` | VARCHAR(120) | e.g. `wikipedia`, `wikidata`, `web:metmuseum.org` | +| `confidence` | VARCHAR(20) | `curated` (default) or `discovered` | + +Unique index on `(painting_id, source_type, source_painting_id, source_artist_id, source_movement_id)` with COALESCE for null FKs. + +Used by painting detail API (`influencedBy`). Painting-type rows also feed `has_influence_links` and hall navigation (with legacy `painting_influences`). ## Indexes @@ -122,6 +153,7 @@ Used by the 3D gallery exit panel: predecessors are artists of `influenced_by_pa - `paintings(artist_id)`, `paintings(period_id)` - `art_movements(era_id)`, `art_movements(start_year, end_year)` - `painting_influences(painting_id)`, `painting_influences(influenced_by_painting_id)` +- `painting_influence_sources(painting_id)`, `painting_influence_sources(source_artist_id)`, `painting_influence_sources(source_movement_id)` ## First-time setup diff --git a/Documentation/basics.md b/Documentation/basics.md index 4f9973c..1e4ae18 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -9,7 +9,7 @@ The app is organised as a **drill-down hierarchy**: 1. **Timeline** — historical eras (Ancient → Contemporary) with definite or fuzzy date boundaries. 2. **Movement flow** — art movements as curved SVG streams on the same year axis; documented predecessor→successor branches; portrait thumbnails placed along each stream. 3. **3D gallery** — one personal hall per artist; paintings on the walls, open centre, single exit for influence-based navigation. -4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right, prev/next catalog browsing, optional fullscreen, link to artist biography. +4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right (paintings, artists, or movements), prev/next catalog browsing, optional fullscreen, link to artist biography. 5. **Artist biography** — portrait, lifespan, movement, and Wikipedia-sourced intro text (`bio_short` / `bio_full`). All artwork images are stored locally under `data/images/` — the UI never hot-links to Wikipedia or Commons at runtime (except optional on-demand fetch when a file is missing). @@ -149,7 +149,7 @@ Each artist appears as a **portrait circle** on their movement’s stream row: Artist portraits stop wheel/drag propagation so zooming over a face does not fight portrait clicks. -**Note:** Movement lineage is **frontend curation** for layout and labels — it is not stored in PostgreSQL. Painting-to-painting influence edges (`painting_influences`) are separate and drive the 3D exit picker and detail panels. +**Note:** Movement lineage is **frontend curation** for layout and labels — it is not stored in PostgreSQL. Painting influence links (`painting_influence_sources`, plus legacy `painting_influences` for hall navigation) are separate and drive the 3D exit picker and detail panels. ## Virtual gallery (3D halls) @@ -191,7 +191,7 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another | Layer | What you see | |-------|----------------| -| **Detail** | Centre image, *Influenced By* (left) and *Influenced* (right), position in catalog (e.g. `3 of 12`) | +| **Detail** | Centre image, *Influenced By* (left) and *Influenced* (right) — painting thumbnails, artist portraits, or movement swatches — position in catalog (e.g. `3 of 12`) | | **Fullscreen** | Click the centre image; `Escape` or click anywhere to return to detail only | **Controls:** @@ -202,6 +202,7 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another | `←` / `→` | Same as prev / next (disabled while fullscreen is open) | | Click centre image | Open fullscreen lightbox | | Click influence thumbnail | Open that work’s detail (different artist allowed) | +| Click influence artist portrait | Open that artist’s 3D gallery hall | | **← Back to Gallery** | Return to the hall you entered from — **3D camera position is preserved** | | **About {artist}** | Open artist biography | @@ -217,9 +218,9 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another - **Movement filtering** on zoom only shows movements that have at least one artist active in the visible year range. - **Movement lineage** (`movement-lineage.ts`) documents art-historical predecessor→successor links for the flow diagram; extend that file to add or correct branches. - **One hall per artist** keeps navigation predictable: enter from the timeline or bio, leave via the single exit or back button. -- **Influence-based hall links** connect artists through documented painting-to-painting relationships, grouped by movement at the exit. +- **Influence-based hall links** connect artists through documented painting relationships, grouped by movement at the exit. - **3D gallery images** use locally cached files only; slow remote fetches would break realtime rendering. -- **Influence data** is stored as directed edges between paintings, with optional citation fields (source author, quote, URL) for art-historical references. +- **Influence data** is stored as directed links from paintings to sources (another painting, an artist, or a movement), with optional period fields and citation metadata (source author, quote, URL). ## Related docs diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md index 1484e8b..5e767a2 100644 --- a/Documentation/data-and-images.md +++ b/Documentation/data-and-images.md @@ -113,22 +113,68 @@ Renaissance and medieval masters with large museum catalog dumps (e.g. Raphael, ## Painting influence graph -Directed edges in `painting_influences` drive: +Directed influence links are stored in **`painting_influence_sources`**. Each row connects a painting to a **source** of type `painting`, `artist`, or `movement`, with optional period context (e.g. influence during the work’s creation year). -- **Painting detail** — *Influenced By* (left) and *Influenced* (right) panels with notes, aspects, and citations -- **3D hall exit** — predecessor and successor artists grouped by movement -- **3D gallery lamps** — a golden picture light above any frame whose work has an influence edge (`has_influence_links` on painting API responses) +The legacy **`painting_influences`** table (painting-to-painting only) is still written for hall navigation compatibility and is backfilled into `painting_influence_sources` on migration. + +Influence data drives: + +- **Painting detail** — *Influenced By* (left) and *Influenced* (right) panels: painting thumbnails, artist portraits, or movement colour swatches, plus notes, aspects, period labels, and citations +- **3D hall exit** — predecessor and successor artists grouped by movement (painting edges only) +- **3D gallery lamps** — golden picture light above frames with any influence edge (`has_influence_links` on painting API responses) + +### One-time migration + +```bash +npm run migrate:influence-sources # create table + backfill legacy painting edges +``` + +### Curated updates `npm run update-influences` runs `scripts/update-influences.js` against `scripts/art-influences-data.js`: ```bash -npm run update-influences # insert edges; create missing artists/paintings -npm run update-influences -- --fetch-images # also download images for newly created works +npm run update-influences # insert curated edges +npm run update-influences -- --fetch-images # also download images for newly created works +npm run update-influences -- --discover # curated + web discovery pass +npm run discover-influences # discovery only (no curated file pass) +npm run update-influences -- --discover --limit=20 # cap discovery to N works ``` -Each entry defines a later `work` influenced by an earlier `influencedBy` painting, plus optional curator fields (`notes`, `aspects`, `source_author`, `source`). When `artistMeta` is included, missing artists are created with movement and lifespan. Missing paintings are inserted with `wikipedia_title` for image fetch. +Each entry defines a later `work` and one or more `influencedBy` sources. Legacy single-object form is still supported: -Extend the data file to add lineage chains (e.g. Giotto → Masaccio → Michelangelo → Manet → Picasso → Warhol). New bridge artists may include Géricault, Friedrich, Constable, Giorgione, Poussin, de Chirico, and Böcklin. +```javascript +{ work: { artist: '...', title: '...', year: 1907 }, + influencedBy: { artist: 'Giotto', title: 'Lamentation' } } +``` + +Multi-source form (painting, artist, movement): + +```javascript +{ + work: { artist: 'Pablo Picasso', title: "Les Demoiselles d'Avignon", year: 1907 }, + influencedBy: [ + { type: 'painting', artist: 'Paul Cézanne', title: 'The Bathers' }, + { type: 'artist', artist: 'Paul Cézanne', period: { duringCreation: true } }, + { type: 'movement', movement: 'Fauvism', period: { start: 1905, end: 1907, note: '...' } }, + ], + notes, aspects, source_author, source, source_url, +} +``` + +When `artistMeta` is included, missing artists are created with movement and lifespan. Missing paintings are inserted with `wikipedia_title` for image fetch. + +### Web discovery + +`scripts/influence-discovery.js` searches art-history sources when `--discover` or `--discover-only` is passed: + +- Wikipedia summaries and Wikidata **P737** (influenced by) +- Met Museum collection API +- DuckDuckGo site-restricted search across TheArtStory, Met, Google Arts & Culture, NGA, Art Institute of Chicago, MoMA, Britannica, JSTOR, Oxford Art Online, WikiArt, and Wikipedia + +Discovered rows are stored with `confidence: discovered` and `discovered_via` (e.g. `wikipedia`, `wikidata`, `met`, `web:theartstory.org`). Period hints are inferred when the source text mentions influence during creation or a date range overlapping the work’s year. + +Extend `art-influences-data.js` for high-quality curated chains; use discovery to suggest additional artist and movement links for manual review. ## Movement lineage (frontend flow diagram) diff --git a/Documentation/setup.md b/Documentation/setup.md index 5aa2a98..196b11b 100644 --- a/Documentation/setup.md +++ b/Documentation/setup.md @@ -111,8 +111,11 @@ Open http://localhost:3001 (or your configured `PORT`). | `npm run migrate:thumbnails` | `db/migrate-thumbnails.sql` | Add thumbnail columns | | `npm run fetch-artist-images` | `scripts/fetch-artist-images.js` | Backfill portrait files *(if present)* | | `npm run sync-image-paths` | `scripts/sync-image-paths.js` | Align DB paths with disk *(if present)* | -| `npm run update-influences` | `scripts/update-influences.js` | Insert influence edges from `art-influences-data.js` | +| `npm run migrate:influence-sources` | `scripts/migrate-influence-sources.js` | Create `painting_influence_sources` + backfill legacy edges | +| `npm run update-influences` | `scripts/update-influences.js` | Insert influence links (painting / artist / movement) from `art-influences-data.js` | | `npm run update-influences -- --fetch-images` | ↑ | Also download images for newly created works | +| `npm run update-influences -- --discover` | ↑ | Curated pass + web discovery (Wikipedia, Wikidata, Met, art-history sites) | +| `npm run discover-influences` | `update-influences.js --discover-only` | Discovery pass only | ### Scripts in the repository @@ -121,7 +124,8 @@ These are checked in and maintained: - `image-fetcher.js` — Wikimedia / museum image resolution - `fetch-artist-bios.js` — artist biographies - `expand-paintings.js` + `famous-paintings-data.js` — catalog expansion -- `update-influences.js` + `art-influences-data.js` — painting influence graph +- `update-influences.js` + `art-influences-data.js` — influence graph (paintings, artists, movements) +- `influence-discovery.js` + `influence-resolver.js` — web discovery and polymorphic source resolution - `fetch-missing-images.js` — batch image backfill - `regenerate-thumbnails.js`, `audit-painting-images.js` @@ -153,7 +157,7 @@ After clone: copy `.env.example` → `.env`, install dependencies, run `npm run | Wrong painting in 3D gallery frame | Stale or mismatched thumbnail file | `npm run regenerate-thumbnails`; gallery prefers full `image_path` | | Wrong painting image (fetch) | Bad museum / search match on first download | Add entry to `DIRECT_IMAGE_OVERRIDES` in `scripts/image-fetcher.js`, re-fetch file | | Empty exit navigation lists | No `painting_influences` edges for artist | `npm run update-influences` | -| Empty *Influenced By* / *Influenced* on painting detail | No edges for that work | `npm run update-influences`; extend `art-influences-data.js` | +| Empty *Influenced By* / *Influenced* on painting detail | No edges for that work | `npm run migrate:influence-sources` then `npm run update-influences`; extend `art-influences-data.js` or run `--discover` | | No golden lamps above frames in 3D hall | Stale API process or no influence edges | Restart server after API changes; run `npm run update-influences` | | Default Vite page instead of gallery | `client/dist` missing or stale | `cd client && npm run build` | | Permission denied creating tables | `gallery` user lacks CREATE | Run admin grants, then migrate | diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 9ad02f7..6d8a2cd 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -46,6 +46,34 @@ export async function preloadArtistImages(artistId: number): Promise<{ fetched: return res.json(); } +export interface DebugImageSearchResult { + query: string; + imageUrl: string | null; + searchUrl: string; + source: string; + sourceLabel?: string; + thumbUrl?: string; +} + +export interface PaintingCheckupRow { + id: number; + title: string; + artist_name: string; + year: number | null; + gallery_file: string | null; + gallery_preview: string | null; + detail_file: string; + detail_preview: string | null; + detail_on_demand: boolean; + gallery_file_exists: boolean; + detail_file_exists: boolean | null; +} + +export interface PaintingCheckupData { + paintings: PaintingCheckupRow[]; + total: number; +} + export const api = { getBounds: () => fetchJson(`${API}/bounds`), @@ -66,4 +94,26 @@ export const api = { fetchJson(`${API}/artists/${id}/navigation`), getPainting: (id: number) => fetchJson(`${API}/paintings/${id}`), + + getPaintingDebugImageSearch: (id: number) => + fetchJson(`${API}/paintings/${id}/debug-image-search`), + + fixPaintingImage: (id: number, imageUrl: string) => + fetch(`${API}/paintings/${id}/fix-image`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ imageUrl }), + }).then(async (res) => { + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Fix failed: ${res.status}`); + } + return res.json() as Promise<{ imagePath: string; thumbnailPath: string }>; + }), + + getPaintingCheckup: () => fetchJson(`${API}/paintings/checkup`), }; + +export function debugImageProxyUrl(imageUrl: string): string { + return `${API}/debug/image-proxy?url=${encodeURIComponent(imageUrl)}`; +} diff --git a/client/src/components/PaintingDetail.css b/client/src/components/PaintingDetail.css index 0fc3fb1..ce1770a 100644 --- a/client/src/components/PaintingDetail.css +++ b/client/src/components/PaintingDetail.css @@ -1,4 +1,5 @@ .painting-detail { + position: relative; min-height: 100vh; background: linear-gradient(180deg, #1a1a2e 0%, #0f0f1a 100%); color: #e8d5b5; @@ -164,6 +165,48 @@ color: #c9a96e; } +.influence-title-static { + cursor: default; +} + +.influence-card-movement { + display: flex; + flex-direction: row; +} + +.influence-movement-swatch { + width: 12px; + flex-shrink: 0; +} + +.influence-card-artist .influence-portrait-btn img { + height: 120px; + object-fit: cover; + object-position: top center; +} + +.influence-period { + font-size: 11px; + color: #a89070; + font-style: italic; + margin: 0; +} + +.influence-discovered-tag { + font-size: 10px; + color: #7a6a55; + font-style: italic; +} + +.influence-title-btn:disabled { + cursor: default; + opacity: 0.85; +} + +.influence-image-btn:disabled { + cursor: default; +} + .influence-aspects { display: flex; flex-wrap: wrap; @@ -366,6 +409,80 @@ color: rgba(232, 213, 181, 0.85); } +.debug-image-panel { + position: fixed; + left: 16px; + bottom: 16px; + z-index: 60; + width: min(280px, calc(100vw - 32px)); + padding: 12px; + border-radius: 8px; + border: 1px solid rgba(232, 160, 64, 0.45); + background: rgba(15, 15, 26, 0.94); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + font-family: ui-monospace, 'Cascadia Code', monospace; +} + +.debug-image-panel h4 { + margin: 0 0 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #e8a040; +} + +.debug-image-query { + margin: 0 0 8px; + font-size: 10px; + line-height: 1.4; + color: rgba(232, 213, 181, 0.75); + word-break: break-word; +} + +.debug-image-preview { + display: block; + width: 100%; + max-height: 160px; + object-fit: contain; + background: #2a1f15; + border-radius: 4px; + margin-bottom: 8px; +} + +.debug-fix-btn { + width: 100%; + padding: 8px 10px; + border: 1px solid #e8a040; + border-radius: 6px; + background: rgba(232, 160, 64, 0.15); + color: #e8a040; + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.debug-fix-btn:hover:not(:disabled) { + background: rgba(232, 160, 64, 0.28); +} + +.debug-fix-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +.debug-image-status { + margin: 0; + font-size: 10px; + color: rgba(201, 169, 110, 0.7); +} + +.debug-image-error { + margin: 0 0 8px; + font-size: 10px; + color: #ff8a80; +} + @media (max-width: 1024px) { .painting-layout { grid-template-columns: 1fr; diff --git a/client/src/components/PaintingDetail.tsx b/client/src/components/PaintingDetail.tsx index 17972e3..2229623 100644 --- a/client/src/components/PaintingDetail.tsx +++ b/client/src/components/PaintingDetail.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type SyntheticEvent } from 'react'; -import type { Painting, PaintingDetail } from '../types'; -import { paintingImageUrl } from '../api/client'; +import type { InfluenceLink, Painting, PaintingDetail } from '../types'; +import { api, debugImageProxyUrl, imageUrl, paintingImageUrl, type DebugImageSearchResult } from '../api/client'; import PaintingLightbox from './PaintingLightbox'; import './PaintingDetail.css'; @@ -11,30 +11,148 @@ interface Props { onPaintingClick: (paintingId: number) => void; onCatalogNavigate: (paintingId: number) => void; onArtistBio: () => void; + onInfluenceArtistClick?: (artistId: number) => void; + debugMode?: boolean; + onPaintingImageFixed?: (paintingId: number) => void | Promise; +} + +function influenceKey(inf: InfluenceLink, index: number): string { + if (inf.source_type === 'movement') return `movement-${inf.movement_id ?? inf.movement_name}-${index}`; + if (inf.source_type === 'artist') return `artist-${inf.source_artist_id ?? inf.source_artist_name}-${index}`; + return `painting-${inf.id}-${index}`; +} + +function periodLabel(inf: InfluenceLink): string | null { + if (inf.period_note) return inf.period_note; + if (inf.period_start_year != null && inf.period_end_year != null) { + return `${inf.period_start_year}–${inf.period_end_year}`; + } + if (inf.period_start_year != null) return `from ${inf.period_start_year}`; + return null; } function InfluenceCard({ inf, onPaintingClick, + onInfluenceArtistClick, }: { - inf: PaintingDetail['influencedBy'][0]; + inf: InfluenceLink; onPaintingClick: (id: number) => void; + onInfluenceArtistClick?: (artistId: number) => void; }) { const aspects = inf.aspects ? inf.aspects.split(',').map((a) => a.trim()).filter(Boolean) : []; + const period = periodLabel(inf); + const sourceType = inf.source_type || 'painting'; + + const meta = ( + <> + {aspects.length > 0 && ( +
+ {aspects.map((aspect) => ( + {aspect} + ))} +
+ )} + {period &&

{period}

} + {inf.notes &&

{inf.notes}

} + {inf.quote && ( +
+

“{inf.quote}”

+ {(inf.source_author || inf.source) && ( +
+ — {inf.source_author} + {inf.source && , {inf.source}} +
+ )} +
+ )} + {inf.source_url && ( + e.stopPropagation()} + > + Read source: {inf.source_author || inf.source || 'Reference'} + + )} + {inf.confidence === 'discovered' && inf.discovered_via && ( + Discovered via {inf.discovered_via} + )} + + ); + + if (sourceType === 'movement') { + return ( +
+
+
+
+ {inf.movement_name} + Art movement +
+ {meta} +
+
+ ); + } + + if (sourceType === 'artist') { + const artistId = inf.source_artist_id; + const artistName = inf.source_artist_name || 'Unknown artist'; + return ( +
+ +
+ + {meta} +
+
+ ); + } + + if (!inf.id) return null; return (
- - - {aspects.length > 0 && ( -
- {aspects.map((aspect) => ( - {aspect} - ))} -
- )} - - {inf.notes &&

{inf.notes}

} - - {inf.quote && ( -
-

“{inf.quote}”

- {(inf.source_author || inf.source) && ( -
- — {inf.source_author} - {inf.source && , {inf.source}} -
- )} -
- )} - - {inf.source_url && ( - e.stopPropagation()} - > - Read source: {inf.source_author || inf.source || 'Reference'} - - )} + {meta}
); @@ -92,10 +177,19 @@ export default function PaintingDetailView({ onPaintingClick, onCatalogNavigate, onArtistBio, + onInfluenceArtistClick, + debugMode = false, + onPaintingImageFixed, }: Props) { const { painting, influencedBy, influenced } = data; const [fullscreen, setFullscreen] = useState(false); - const imageSrc = paintingImageUrl(painting); + const [imageVersion, setImageVersion] = useState(0); + const [debugSearch, setDebugSearch] = useState(null); + const [debugLoading, setDebugLoading] = useState(false); + const [debugError, setDebugError] = useState(null); + const [fixing, setFixing] = useState(false); + + const imageSrc = `${paintingImageUrl(painting)}${paintingImageUrl(painting).includes('?') ? '&' : '?'}v=${imageVersion}`; const catalogIndex = artistPaintings.findIndex((p) => p.id === painting.id); const previousPainting = catalogIndex > 0 ? artistPaintings[catalogIndex - 1] : null; @@ -107,8 +201,52 @@ export default function PaintingDetailView({ useEffect(() => { setFullscreen(false); + setImageVersion(0); }, [painting.id]); + useEffect(() => { + if (!debugMode) { + setDebugSearch(null); + setDebugError(null); + return; + } + + let cancelled = false; + setDebugLoading(true); + setDebugError(null); + setDebugSearch(null); + + api.getPaintingDebugImageSearch(painting.id) + .then((result) => { + if (!cancelled) setDebugSearch(result); + }) + .catch(() => { + if (!cancelled) setDebugError('Google image search failed.'); + }) + .finally(() => { + if (!cancelled) setDebugLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [debugMode, painting.id, painting.title, painting.artist_name]); + + const handleFixImage = async () => { + if (!debugSearch?.imageUrl || fixing) return; + setFixing(true); + setDebugError(null); + try { + await api.fixPaintingImage(painting.id, debugSearch.imageUrl); + setImageVersion((v) => v + 1); + await onPaintingImageFixed?.(painting.id); + } catch (err) { + setDebugError(err instanceof Error ? err.message : 'Could not replace image.'); + } finally { + setFixing(false); + } + }; + useEffect(() => { if (fullscreen) return; @@ -159,8 +297,13 @@ export default function PaintingDetailView({

No documented influences for this work.

) : (
- {influencedBy.map((inf) => ( - + {influencedBy.map((inf, index) => ( + ))}
)} @@ -226,14 +369,53 @@ export default function PaintingDetailView({

No documented works influenced by this painting yet.

) : (
- {influenced.map((inf) => ( - + {influenced.map((inf, index) => ( + ))}
)} + {debugMode && ( + + )} + {fullscreen && ( void; + onOpenPainting?: (paintingId: number) => void; +} + +function previewSrc(row: PaintingCheckupRow, kind: 'gallery' | 'detail'): string | null { + if (kind === 'gallery') { + const file = row.gallery_preview ?? row.gallery_file; + return file ? `/images/${file}` : null; + } + if (row.detail_on_demand) { + return `/api/paintings/${row.id}/image?size=thumb`; + } + const file = row.detail_preview ?? row.detail_file; + return file ? `/images/${file}` : null; +} + +function ImagePreviewCell({ + row, + kind, +}: { + row: PaintingCheckupRow; + kind: 'gallery' | 'detail'; +}) { + const path = kind === 'gallery' ? row.gallery_file : row.detail_file; + const src = previewSrc(row, kind); + const exists = + kind === 'gallery' ? row.gallery_file_exists : row.detail_file_exists; + const missing = path && exists === false; + + if (!path) { + return ( +
+ No image + Hidden in 3D +
+ ); + } + + return ( +
+ {src && ( + { + (e.target as HTMLImageElement).src = '/placeholder-art.svg'; + }} + /> + )} + {missing && missing file} + {row.detail_on_demand && kind === 'detail' && ( + on-demand + )} +
+ ); +} + +export default function CheckupPage({ onBack, onOpenPainting }: Props) { + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filter, setFilter] = useState(''); + + useEffect(() => { + let cancelled = false; + setLoading(true); + api.getPaintingCheckup() + .then((data) => { + if (!cancelled) { + setRows(data.paintings); + setError(null); + } + }) + .catch(() => { + if (!cancelled) setError('Could not load checkup data. Is the server running?'); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const filtered = useMemo(() => { + const q = filter.trim().toLowerCase(); + if (!q) return rows; + return rows.filter( + (row) => + row.title.toLowerCase().includes(q) || + row.artist_name.toLowerCase().includes(q) || + String(row.year ?? '').includes(q) || + (row.gallery_file ?? '').toLowerCase().includes(q) || + row.detail_file.toLowerCase().includes(q) + ); + }, [rows, filter]); + + const stats = useMemo(() => { + const noGallery = rows.filter((r) => !r.gallery_file).length; + const onDemand = rows.filter((r) => r.detail_on_demand).length; + const missingGallery = rows.filter((r) => r.gallery_file && !r.gallery_file_exists).length; + const missingDetail = rows.filter( + (r) => r.detail_file_exists === false + ).length; + return { noGallery, onDemand, missingGallery, missingDetail }; + }, [rows]); + + return ( +
+
+ +
+

Painting checkup

+

Compare image files used in the 3D gallery vs painting detail view.

+
+
+ +
+ setFilter(e.target.value)} + /> +
+ {filtered.length} shown + {stats.noGallery} no gallery file + {stats.onDemand} detail on-demand + {stats.missingGallery} gallery missing + {stats.missingDetail} detail missing +
+
+ + {error &&
{error}
} + + {loading ? ( +
Loading paintings…
+ ) : ( +
+ + + + + + + + + + + + {filtered.map((row) => ( + + + + + + + + ))} + +
PaintingArtistYearGalleryDetail
+ {onOpenPainting ? ( + + ) : ( + row.title + )} + {row.artist_name}{row.year ?? '—'} + + + +
+
+ )} +
+ ); +} diff --git a/client/src/pages/HomePage.css b/client/src/pages/HomePage.css index 4f357bb..7658c34 100644 --- a/client/src/pages/HomePage.css +++ b/client/src/pages/HomePage.css @@ -32,6 +32,45 @@ text-align: center; padding: 24px 16px 8px; flex-shrink: 0; + position: relative; +} + +.site-dev-tools { + position: absolute; + top: 16px; + right: 16px; + display: flex; + gap: 8px; + align-items: center; +} + +.debug-mode-toggle, +.checkup-link-btn { + padding: 6px 12px; + border-radius: 6px; + border: 1px solid rgba(201, 169, 110, 0.35); + background: rgba(15, 15, 26, 0.85); + color: rgba(201, 169, 110, 0.75); + font-size: 12px; + font-family: ui-monospace, 'Cascadia Code', monospace; + cursor: pointer; + transition: background 0.2s, border-color 0.2s, color 0.2s; +} + +.debug-mode-toggle:hover, +.checkup-link-btn:hover { + border-color: #c9a96e; + color: #e8d5b5; +} + +.debug-mode-toggle-active { + border-color: #e8a040; + background: rgba(232, 160, 64, 0.15); + color: #e8a040; +} + +.checkup-link-btn { + text-decoration: none; } .site-header h1 { diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index fe92e7f..a4892cf 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -4,13 +4,16 @@ import MovementBands from '../components/MovementBands'; import VirtualGallery from '../components/VirtualGallery'; import PaintingDetailView from '../components/PaintingDetail'; import ArtistBio from '../components/ArtistBio'; +import CheckupPage from '../pages/CheckupPage'; import { api } from '../api/client'; import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail } from '../types'; import { sortArtistPaintingsChronological } from '../utils/paintingUtils'; +import { readDebugMode, writeDebugMode } from '../utils/debugMode'; import './HomePage.css'; type View = | { type: 'timeline' } + | { type: 'checkup' } | { type: 'gallery'; artistId: number; data: ArtistDetail } | { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View } | { type: 'bio'; artistId: number; data: ArtistDetail; returnTo: View }; @@ -28,6 +31,7 @@ export default function HomePage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [detailArtistPaintings, setDetailArtistPaintings] = useState([]); + const [debugMode, setDebugMode] = useState(readDebugMode); const detailReturnToRef = useRef({ type: 'timeline' }); useEffect(() => { @@ -76,6 +80,51 @@ export default function HomePage() { setViewEnd(end); }; + const toggleDebugMode = () => { + setDebugMode((prev) => { + const next = !prev; + writeDebugMode(next); + return next; + }); + }; + + const handlePaintingImageFixed = useCallback(async (paintingId: number) => { + const data = await api.getPainting(paintingId); + setView((current) => + current.type === 'painting' && current.paintingId === paintingId + ? { ...current, data } + : current + ); + setDetailArtistPaintings((list) => + list.map((p) => + p.id === paintingId + ? { ...p, image_path: data.painting.image_path, thumbnail_path: data.painting.thumbnail_path } + : p + ) + ); + if (gallerySession?.artistId === data.painting.artist_id) { + setGallerySession((session) => + session + ? { + ...session, + data: { + ...session.data, + paintings: session.data.paintings.map((p) => + p.id === paintingId + ? { + ...p, + image_path: data.painting.image_path, + thumbnail_path: data.painting.thumbnail_path, + } + : p + ), + }, + } + : session + ); + } + }, [gallerySession]); + const handleArtistClick = async (artistId: number) => { try { const data = await api.getArtist(artistId); @@ -182,6 +231,9 @@ export default function HomePage() { const artistData = await api.getArtist(view.data.painting.artist_id); setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo: view }); }} + onInfluenceArtistClick={handleArtistClick} + debugMode={debugMode} + onPaintingImageFixed={handlePaintingImageFixed} /> )} @@ -198,9 +250,34 @@ export default function HomePage() { )} + {view.type === 'checkup' && ( + setView({ type: 'timeline' })} + onOpenPainting={handlePaintingClick} + /> + )} + {view.type === 'timeline' && (
+
+ + +

Virtual Art Gallery

Watch art movements branch forward through time — each flowing from what came before

diff --git a/client/src/types/index.ts b/client/src/types/index.ts index 0a4899c..b23a962 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -63,19 +63,33 @@ export interface Painting { has_influence_links?: boolean | string | number; } +export type InfluenceSourceType = 'painting' | 'artist' | 'movement'; + export interface InfluenceLink { - notes: string; - source: string; + source_type: InfluenceSourceType; + notes?: string; + source?: string; aspects?: string; quote?: string; source_author?: string; source_url?: string; - id: number; - title: string; - year: number; - image_path: string; - artist_name: string; - artist_id: number; + confidence?: string; + discovered_via?: string; + period_note?: string; + period_start_year?: number | null; + period_end_year?: number | null; + id?: number; + title?: string; + year?: number | null; + image_path?: string | null; + artist_name?: string; + artist_id?: number; + source_artist_id?: number; + source_artist_name?: string; + artist_portrait?: string | null; + movement_id?: number; + movement_name?: string; + movement_color?: string; } export interface PaintingDetail { diff --git a/client/src/utils/debugMode.ts b/client/src/utils/debugMode.ts new file mode 100644 index 0000000..338fdad --- /dev/null +++ b/client/src/utils/debugMode.ts @@ -0,0 +1,17 @@ +const DEBUG_MODE_KEY = 'gallery-debug-mode'; + +export function readDebugMode(): boolean { + try { + return localStorage.getItem(DEBUG_MODE_KEY) === '1'; + } catch { + return false; + } +} + +export function writeDebugMode(enabled: boolean): void { + try { + localStorage.setItem(DEBUG_MODE_KEY, enabled ? '1' : '0'); + } catch { + // ignore + } +} diff --git a/data/images/paintings/Cimabue_Maest__di_Santa_Maria_dei_Servi.jpg b/data/images/paintings/Cimabue_Maest__di_Santa_Maria_dei_Servi.jpg new file mode 100644 index 0000000..ebb6eb5 Binary files /dev/null and b/data/images/paintings/Cimabue_Maest__di_Santa_Maria_dei_Servi.jpg differ diff --git a/data/images/paintings/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints.jpg b/data/images/paintings/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints.jpg new file mode 100644 index 0000000..b79c3cf Binary files /dev/null and b/data/images/paintings/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints.jpg differ diff --git a/data/images/paintings/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child.jpg b/data/images/paintings/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child.jpg new file mode 100644 index 0000000..79e107b Binary files /dev/null and b/data/images/paintings/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child.jpg differ diff --git a/data/images/paintings/Duccio_Angel.jpg b/data/images/paintings/Duccio_Angel.jpg new file mode 100644 index 0000000..03e91af Binary files /dev/null and b/data/images/paintings/Duccio_Angel.jpg differ diff --git a/data/images/paintings/Duccio_Apostles_Philip_and_James_the_Elder.jpg b/data/images/paintings/Duccio_Apostles_Philip_and_James_the_Elder.jpg new file mode 100644 index 0000000..66685b1 Binary files /dev/null and b/data/images/paintings/Duccio_Apostles_Philip_and_James_the_Elder.jpg differ diff --git a/data/images/paintings/Duccio_Disputation_with_the_Doctors.jpg b/data/images/paintings/Duccio_Disputation_with_the_Doctors.jpg new file mode 100644 index 0000000..5443eff Binary files /dev/null and b/data/images/paintings/Duccio_Disputation_with_the_Doctors.jpg differ diff --git a/data/images/paintings/Duccio_Original_rose_window.jpg b/data/images/paintings/Duccio_Original_rose_window.jpg new file mode 100644 index 0000000..0076c6e Binary files /dev/null and b/data/images/paintings/Duccio_Original_rose_window.jpg differ diff --git a/data/images/paintings/Duccio_Rucellai_Madonna.jpg b/data/images/paintings/Duccio_Rucellai_Madonna.jpg new file mode 100644 index 0000000..6b45d15 Binary files /dev/null and b/data/images/paintings/Duccio_Rucellai_Madonna.jpg differ diff --git a/data/images/paintings/Ernst_Ludwig_Kirchner_Potsdamer_Platz.jpg b/data/images/paintings/Ernst_Ludwig_Kirchner_Potsdamer_Platz.jpg new file mode 100644 index 0000000..38a0018 Binary files /dev/null and b/data/images/paintings/Ernst_Ludwig_Kirchner_Potsdamer_Platz.jpg differ diff --git a/data/images/paintings/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid_.jpg b/data/images/paintings/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid_.jpg new file mode 100644 index 0000000..005d796 Binary files /dev/null and b/data/images/paintings/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid_.jpg differ diff --git a/data/images/paintings/Francis_Picabia_Dada_Movement.jpg b/data/images/paintings/Francis_Picabia_Dada_Movement.jpg new file mode 100644 index 0000000..858dfa0 Binary files /dev/null and b/data/images/paintings/Francis_Picabia_Dada_Movement.jpg differ diff --git a/data/images/paintings/Francisco_Goya_The_Third_of_May_1808.jpg b/data/images/paintings/Francisco_Goya_The_Third_of_May_1808.jpg new file mode 100644 index 0000000..b9fb44c Binary files /dev/null and b/data/images/paintings/Francisco_Goya_The_Third_of_May_1808.jpg differ diff --git a/data/images/paintings/Georges_Braque_Man_with_a_Guitar.jpg b/data/images/paintings/Georges_Braque_Man_with_a_Guitar.jpg new file mode 100644 index 0000000..7bbf5f9 Binary files /dev/null and b/data/images/paintings/Georges_Braque_Man_with_a_Guitar.jpg differ diff --git a/data/images/paintings/Gino_Severini_Blue_Dancer.jpg b/data/images/paintings/Gino_Severini_Blue_Dancer.jpg new file mode 100644 index 0000000..e482632 Binary files /dev/null and b/data/images/paintings/Gino_Severini_Blue_Dancer.jpg differ diff --git a/data/images/paintings/Giotto_Canonization_of_St__Francis.jpg b/data/images/paintings/Giotto_Canonization_of_St__Francis.jpg new file mode 100644 index 0000000..fedfa35 Binary files /dev/null and b/data/images/paintings/Giotto_Canonization_of_St__Francis.jpg differ diff --git a/data/images/paintings/Giotto_Stigmatization_of_St_Francis.jpg b/data/images/paintings/Giotto_Stigmatization_of_St_Francis.jpg new file mode 100644 index 0000000..a24858c Binary files /dev/null and b/data/images/paintings/Giotto_Stigmatization_of_St_Francis.jpg differ diff --git a/data/images/paintings/Henri_Matisse_Luxe__Calme_et_Volupt_.jpg b/data/images/paintings/Henri_Matisse_Luxe__Calme_et_Volupt_.jpg new file mode 100644 index 0000000..dd1cd4e Binary files /dev/null and b/data/images/paintings/Henri_Matisse_Luxe__Calme_et_Volupt_.jpg differ diff --git a/data/images/paintings/Henri_Matisse_The_Snail.jpg b/data/images/paintings/Henri_Matisse_The_Snail.jpg new file mode 100644 index 0000000..83ff55d Binary files /dev/null and b/data/images/paintings/Henri_Matisse_The_Snail.jpg differ diff --git a/data/images/paintings/Hieronymus_Bosch_Christ_Mocked.jpg b/data/images/paintings/Hieronymus_Bosch_Christ_Mocked.jpg new file mode 100644 index 0000000..43231f8 Binary files /dev/null and b/data/images/paintings/Hieronymus_Bosch_Christ_Mocked.jpg differ diff --git a/data/images/paintings/Ivan_Klyun_Suprematist_Composition.jpg b/data/images/paintings/Ivan_Klyun_Suprematist_Composition.jpg new file mode 100644 index 0000000..e28ed7c Binary files /dev/null and b/data/images/paintings/Ivan_Klyun_Suprematist_Composition.jpg differ diff --git a/data/images/paintings/J__M__W__Turner_Dido_Building_Carthage.jpg b/data/images/paintings/J__M__W__Turner_Dido_Building_Carthage.jpg new file mode 100644 index 0000000..e37800c Binary files /dev/null and b/data/images/paintings/J__M__W__Turner_Dido_Building_Carthage.jpg differ diff --git a/data/images/paintings/Jackson_Pollock_Blue_Poles.jpg b/data/images/paintings/Jackson_Pollock_Blue_Poles.jpg new file mode 100644 index 0000000..f730f49 Binary files /dev/null and b/data/images/paintings/Jackson_Pollock_Blue_Poles.jpg differ diff --git a/data/images/paintings/Jackson_Pollock_One__Number_31__1950.jpg b/data/images/paintings/Jackson_Pollock_One__Number_31__1950.jpg new file mode 100644 index 0000000..288b4cf Binary files /dev/null and b/data/images/paintings/Jackson_Pollock_One__Number_31__1950.jpg differ diff --git a/data/images/paintings/Jacques-Louis_David_Napoleon_Crossing_the_Alps.jpg b/data/images/paintings/Jacques-Louis_David_Napoleon_Crossing_the_Alps.jpg new file mode 100644 index 0000000..b8077d9 Binary files /dev/null and b/data/images/paintings/Jacques-Louis_David_Napoleon_Crossing_the_Alps.jpg differ diff --git a/data/images/paintings/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women.jpg b/data/images/paintings/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women.jpg new file mode 100644 index 0000000..d7b484c Binary files /dev/null and b/data/images/paintings/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Angel_of_Annunciation.jpg b/data/images/paintings/Jan_van_Eyck_Angel_of_Annunciation.jpg new file mode 100644 index 0000000..cf6fcad Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Angel_of_Annunciation.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Annunciation.jpg b/data/images/paintings/Jan_van_Eyck_Annunciation.jpg new file mode 100644 index 0000000..7e01853 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Annunciation.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Arnolfini_Portrait.jpg b/data/images/paintings/Jan_van_Eyck_Arnolfini_Portrait.jpg new file mode 100644 index 0000000..ff9f7a1 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Arnolfini_Portrait.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Cumaean_Sibyl.jpg b/data/images/paintings/Jan_van_Eyck_Cumaean_Sibyl.jpg new file mode 100644 index 0000000..80a6dcd Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Cumaean_Sibyl.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Dresden_Triptych.jpg b/data/images/paintings/Jan_van_Eyck_Dresden_Triptych.jpg new file mode 100644 index 0000000..0ffb9d7 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Dresden_Triptych.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl.jpg b/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl.jpg new file mode 100644 index 0000000..b019dbb Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view.jpg b/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view.jpg new file mode 100644 index 0000000..52a7ce5 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Lucca_Madonna.jpg b/data/images/paintings/Jan_van_Eyck_Lucca_Madonna.jpg new file mode 100644 index 0000000..f4f63c8 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Lucca_Madonna.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele.jpg b/data/images/paintings/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele.jpg new file mode 100644 index 0000000..1e37481 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Music-making_Angels.jpg b/data/images/paintings/Jan_van_Eyck_Music-making_Angels.jpg new file mode 100644 index 0000000..d7fb7c8 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Music-making_Angels.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_St_Christopher_leading_the_pilgrims.jpg b/data/images/paintings/Jan_van_Eyck_St_Christopher_leading_the_pilgrims.jpg new file mode 100644 index 0000000..f10f9cb Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_St_Christopher_leading_the_pilgrims.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor.jpg b/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor.jpg new file mode 100644 index 0000000..3d18676 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed_.jpg b/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed_.jpg new file mode 100644 index 0000000..c9dd34a Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed_.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_The_Just_Judges.jpg b/data/images/paintings/Jan_van_Eyck_The_Just_Judges.jpg new file mode 100644 index 0000000..9eb3e29 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_The_Just_Judges.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_The_hermits.jpg b/data/images/paintings/Jan_van_Eyck_The_hermits.jpg new file mode 100644 index 0000000..e84894b Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_The_hermits.jpg differ diff --git a/data/images/paintings/Jan_van_Eyck_Virgin_with_Female_Saints.jpg b/data/images/paintings/Jan_van_Eyck_Virgin_with_Female_Saints.jpg new file mode 100644 index 0000000..affb9a2 Binary files /dev/null and b/data/images/paintings/Jan_van_Eyck_Virgin_with_Female_Saints.jpg differ diff --git a/data/images/paintings/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes.jpg b/data/images/paintings/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes.jpg new file mode 100644 index 0000000..80c1c4f Binary files /dev/null and b/data/images/paintings/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes.jpg differ diff --git a/data/images/paintings/Leonardo_da_Vinci_A_Mechanical_Military_Drum.jpg b/data/images/paintings/Leonardo_da_Vinci_A_Mechanical_Military_Drum.jpg new file mode 100644 index 0000000..80c1c4f Binary files /dev/null and b/data/images/paintings/Leonardo_da_Vinci_A_Mechanical_Military_Drum.jpg differ diff --git a/data/images/paintings/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels.jpg b/data/images/paintings/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels.jpg new file mode 100644 index 0000000..80c1c4f Binary files /dev/null and b/data/images/paintings/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels.jpg differ diff --git a/data/images/paintings/Lyubov_Popova_Non-Objective_Composition.jpg b/data/images/paintings/Lyubov_Popova_Non-Objective_Composition.jpg new file mode 100644 index 0000000..1420114 Binary files /dev/null and b/data/images/paintings/Lyubov_Popova_Non-Objective_Composition.jpg differ diff --git a/data/images/paintings/Lyubov_Popova_Spatial_Force_Construction.jpg b/data/images/paintings/Lyubov_Popova_Spatial_Force_Construction.jpg new file mode 100644 index 0000000..1420114 Binary files /dev/null and b/data/images/paintings/Lyubov_Popova_Spatial_Force_Construction.jpg differ diff --git a/data/images/paintings/Mark_Rothko_No__10.jpg b/data/images/paintings/Mark_Rothko_No__10.jpg new file mode 100644 index 0000000..4e9fc33 Binary files /dev/null and b/data/images/paintings/Mark_Rothko_No__10.jpg differ diff --git a/data/images/paintings/Mark_Rothko_Orange_and_Yellow.jpg b/data/images/paintings/Mark_Rothko_Orange_and_Yellow.jpg new file mode 100644 index 0000000..ead1f9a Binary files /dev/null and b/data/images/paintings/Mark_Rothko_Orange_and_Yellow.jpg differ diff --git a/data/images/paintings/Mark_Rothko_Untitled__Black_on_Gray_.jpg b/data/images/paintings/Mark_Rothko_Untitled__Black_on_Gray_.jpg new file mode 100644 index 0000000..ead1f9a Binary files /dev/null and b/data/images/paintings/Mark_Rothko_Untitled__Black_on_Gray_.jpg differ diff --git a/data/images/paintings/Mark_Rothko_White_Center.jpg b/data/images/paintings/Mark_Rothko_White_Center.jpg new file mode 100644 index 0000000..ead1f9a Binary files /dev/null and b/data/images/paintings/Mark_Rothko_White_Center.jpg differ diff --git a/data/images/paintings/Masaccio_Coin_in_the_fish_s_mouth.jpg b/data/images/paintings/Masaccio_Coin_in_the_fish_s_mouth.jpg new file mode 100644 index 0000000..2026b0e Binary files /dev/null and b/data/images/paintings/Masaccio_Coin_in_the_fish_s_mouth.jpg differ diff --git a/data/images/paintings/Masaccio_The_Decapitation_of_Saint_John_the_Baptist.jpg b/data/images/paintings/Masaccio_The_Decapitation_of_Saint_John_the_Baptist.jpg new file mode 100644 index 0000000..965eab0 Binary files /dev/null and b/data/images/paintings/Masaccio_The_Decapitation_of_Saint_John_the_Baptist.jpg differ diff --git a/data/images/paintings/Maurice_de_Vlaminck_The_Red_Trees.jpg b/data/images/paintings/Maurice_de_Vlaminck_The_Red_Trees.jpg new file mode 100644 index 0000000..c9d6500 Binary files /dev/null and b/data/images/paintings/Maurice_de_Vlaminck_The_Red_Trees.jpg differ diff --git a/data/images/paintings/Maurice_de_Vlaminck_The_River_Seine_at_Chatou.jpg b/data/images/paintings/Maurice_de_Vlaminck_The_River_Seine_at_Chatou.jpg new file mode 100644 index 0000000..c9d6500 Binary files /dev/null and b/data/images/paintings/Maurice_de_Vlaminck_The_River_Seine_at_Chatou.jpg differ diff --git a/data/images/paintings/Max_Ernst_Forest_and_Dove.jpg b/data/images/paintings/Max_Ernst_Forest_and_Dove.jpg new file mode 100644 index 0000000..d47583b Binary files /dev/null and b/data/images/paintings/Max_Ernst_Forest_and_Dove.jpg differ diff --git a/data/images/paintings/Michelangelo_A_nude_man_and_two_figures_behind_him.jpg b/data/images/paintings/Michelangelo_A_nude_man_and_two_figures_behind_him.jpg new file mode 100644 index 0000000..d8cc625 Binary files /dev/null and b/data/images/paintings/Michelangelo_A_nude_man_and_two_figures_behind_him.jpg differ diff --git a/data/images/paintings/Michelangelo_Doni_Tondo.png b/data/images/paintings/Michelangelo_Doni_Tondo.png new file mode 100644 index 0000000..55073d7 Binary files /dev/null and b/data/images/paintings/Michelangelo_Doni_Tondo.png differ diff --git a/data/images/paintings/Michelangelo_Erythraean_Sibyl.jpg b/data/images/paintings/Michelangelo_Erythraean_Sibyl.jpg new file mode 100644 index 0000000..b019dbb Binary files /dev/null and b/data/images/paintings/Michelangelo_Erythraean_Sibyl.jpg differ diff --git a/data/images/paintings/Michelangelo_Judith_and_Holofernes.jpg b/data/images/paintings/Michelangelo_Judith_and_Holofernes.jpg new file mode 100644 index 0000000..1a629dd Binary files /dev/null and b/data/images/paintings/Michelangelo_Judith_and_Holofernes.jpg differ diff --git a/data/images/paintings/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso.jpg b/data/images/paintings/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso.jpg new file mode 100644 index 0000000..8fe0346 Binary files /dev/null and b/data/images/paintings/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso.jpg differ diff --git a/data/images/paintings/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto_.tif b/data/images/paintings/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto_.tif new file mode 100644 index 0000000..b97d1f9 Binary files /dev/null and b/data/images/paintings/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto_.tif differ diff --git a/data/images/paintings/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left.jpg b/data/images/paintings/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left.jpg new file mode 100644 index 0000000..37f678b Binary files /dev/null and b/data/images/paintings/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left.jpg differ diff --git a/data/images/paintings/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right.jpg b/data/images/paintings/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right.jpg new file mode 100644 index 0000000..bcf5628 Binary files /dev/null and b/data/images/paintings/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right.jpg differ diff --git a/data/images/paintings/Parmigianino_Madonna_with_the_Long_Neck.jpg b/data/images/paintings/Parmigianino_Madonna_with_the_Long_Neck.jpg new file mode 100644 index 0000000..06cab0e Binary files /dev/null and b/data/images/paintings/Parmigianino_Madonna_with_the_Long_Neck.jpg differ diff --git a/data/images/paintings/Paul_C_zanne_The_Large_Bathers.jpg b/data/images/paintings/Paul_C_zanne_The_Large_Bathers.jpg new file mode 100644 index 0000000..ce10d8e Binary files /dev/null and b/data/images/paintings/Paul_C_zanne_The_Large_Bathers.jpg differ diff --git a/data/images/paintings/Paul_Gauguin_Spirit_of_the_Dead_Watching.jpg b/data/images/paintings/Paul_Gauguin_Spirit_of_the_Dead_Watching.jpg new file mode 100644 index 0000000..bbff140 Binary files /dev/null and b/data/images/paintings/Paul_Gauguin_Spirit_of_the_Dead_Watching.jpg differ diff --git a/data/images/paintings/Ren__Magritte_The_Son_of_Man.jpg b/data/images/paintings/Ren__Magritte_The_Son_of_Man.jpg new file mode 100644 index 0000000..c4185e2 Binary files /dev/null and b/data/images/paintings/Ren__Magritte_The_Son_of_Man.jpg differ diff --git a/data/images/paintings/Richard_Hamilton_Hers_is_a_Luscious_Situation.jpg b/data/images/paintings/Richard_Hamilton_Hers_is_a_Luscious_Situation.jpg new file mode 100644 index 0000000..8653c0e Binary files /dev/null and b/data/images/paintings/Richard_Hamilton_Hers_is_a_Luscious_Situation.jpg differ diff --git a/data/images/paintings/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories.jpg b/data/images/paintings/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories.jpg new file mode 100644 index 0000000..12f5a74 Binary files /dev/null and b/data/images/paintings/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories.jpg differ diff --git a/data/images/paintings/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus.jpg b/data/images/paintings/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus.jpg new file mode 100644 index 0000000..0bb61df Binary files /dev/null and b/data/images/paintings/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus.jpg differ diff --git a/data/images/paintings/Simone_Martini_Madonna_of_Mercy.jpg b/data/images/paintings/Simone_Martini_Madonna_of_Mercy.jpg new file mode 100644 index 0000000..812428c Binary files /dev/null and b/data/images/paintings/Simone_Martini_Madonna_of_Mercy.jpg differ diff --git a/data/images/paintings/Theophanes_the_Greek_Theotokos_of_Tolga.jpg b/data/images/paintings/Theophanes_the_Greek_Theotokos_of_Tolga.jpg new file mode 100644 index 0000000..b0b31c8 Binary files /dev/null and b/data/images/paintings/Theophanes_the_Greek_Theotokos_of_Tolga.jpg differ diff --git a/data/images/paintings/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape.jpg b/data/images/paintings/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape.jpg index f759ce2..962f7be 100644 Binary files a/data/images/paintings/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape.jpg and b/data/images/paintings/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape.jpg differ diff --git a/data/images/paintings/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr.jpg b/data/images/paintings/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr.jpg new file mode 100644 index 0000000..5762b1d Binary files /dev/null and b/data/images/paintings/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr.jpg differ diff --git a/data/images/paintings/Wassily_Kandinsky_Improvisation_28.jpg b/data/images/paintings/Wassily_Kandinsky_Improvisation_28.jpg new file mode 100644 index 0000000..1bba5bb Binary files /dev/null and b/data/images/paintings/Wassily_Kandinsky_Improvisation_28.jpg differ diff --git a/data/images/paintings/Willem_de_Kooning_Woman_I.jpg b/data/images/paintings/Willem_de_Kooning_Woman_I.jpg new file mode 100644 index 0000000..56021cb Binary files /dev/null and b/data/images/paintings/Willem_de_Kooning_Woman_I.jpg differ diff --git a/data/images/paintings/Zeuxis_Centaur_Family.jpg b/data/images/paintings/Zeuxis_Centaur_Family.jpg new file mode 100644 index 0000000..1525856 Binary files /dev/null and b/data/images/paintings/Zeuxis_Centaur_Family.jpg differ diff --git a/data/images/paintings/_douard_Manet_Luncheon_on_the_Grass.jpg b/data/images/paintings/_douard_Manet_Luncheon_on_the_Grass.jpg new file mode 100644 index 0000000..82f10ac Binary files /dev/null and b/data/images/paintings/_douard_Manet_Luncheon_on_the_Grass.jpg differ diff --git a/data/images/paintings/_douard_Manet_Olympia.jpg b/data/images/paintings/_douard_Manet_Olympia.jpg new file mode 100644 index 0000000..1e299b1 Binary files /dev/null and b/data/images/paintings/_douard_Manet_Olympia.jpg differ diff --git a/data/images/paintings/_douard_Manet_The_Balcony.jpg b/data/images/paintings/_douard_Manet_The_Balcony.jpg new file mode 100644 index 0000000..96438b2 Binary files /dev/null and b/data/images/paintings/_douard_Manet_The_Balcony.jpg differ diff --git a/data/images/paintings/thumbs/Cimabue_Maest__di_Santa_Maria_dei_Servi_thumb.jpg b/data/images/paintings/thumbs/Cimabue_Maest__di_Santa_Maria_dei_Servi_thumb.jpg new file mode 100644 index 0000000..3566957 Binary files /dev/null and b/data/images/paintings/thumbs/Cimabue_Maest__di_Santa_Maria_dei_Servi_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_A_Legend_of_Saints_Justus_and_Clement_of_Volterra_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_A_Legend_of_Saints_Justus_and_Clement_of_Volterra_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_A_Legend_of_Saints_Justus_and_Clement_of_Volterra_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Announcement_of_Death_to_St_Fina_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Announcement_of_Death_to_St_Fina_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Announcement_of_Death_to_St_Fina_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Assassination_of_Saint_Peter_the_Martyr_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Assassination_of_Saint_Peter_the_Martyr_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Assassination_of_Saint_Peter_the_Martyr_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Copy_of_a_Roman_Statue_of_a_Seated_Woman_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Copy_of_a_Roman_Statue_of_a_Seated_Woman_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Copy_of_a_Roman_Statue_of_a_Seated_Woman_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Drapery_study__possibly_study_of_the_robe_of_Matthew_the_Evangelist__thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Drapery_study__possibly_study_of_the_robe_of_Matthew_the_Evangelist__thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Drapery_study__possibly_study_of_the_robe_of_Matthew_the_Evangelist__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints_thumb.jpg new file mode 100644 index 0000000..b102111 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_and_Child_Enthroned_with_Saints_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_of_Mercy_and_Lamentation_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_of_Mercy_and_Lamentation_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Madonna_of_Mercy_and_Lamentation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child_thumb.jpg new file mode 100644 index 0000000..9d8dcf3 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Mary__John_and_Christ_as_a_Child_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Nativity_of_Mary_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Nativity_of_Mary_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Nativity_of_Mary_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Renunciation_of_Worldly_Goods_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Renunciation_of_Worldly_Goods_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Renunciation_of_Worldly_Goods_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Sacred_Conversation_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Sacred_Conversation_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Sacred_Conversation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Dominic_tests_the_books_in_the_fire_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Dominic_tests_the_books_in_the_fire_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Dominic_tests_the_books_in_the_fire_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Jerome_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Jerome_thumb.jpg new file mode 100644 index 0000000..7098ca2 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Saint_Jerome_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Slaughter_of_the_Innocents_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Slaughter_of_the_Innocents_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Slaughter_of_the_Innocents_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_St__John_in_the_desert_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_St__John_in_the_desert_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_St__John_in_the_desert_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Three_saints_fresco_by_Ghirlandaio_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Three_saints_fresco_by_Ghirlandaio_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Three_saints_fresco_by_Ghirlandaio_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Two_Women_in_Fluttering_Draperies_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Two_Women_in_Fluttering_Draperies_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Two_Women_in_Fluttering_Draperies_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Virgin_and_Child_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Virgin_and_Child_thumb.jpg new file mode 100644 index 0000000..6c757d5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Virgin_and_Child_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_the_four_evangelists_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_the_four_evangelists_thumb.jpg new file mode 100644 index 0000000..2a6d9e5 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_the_four_evangelists_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_Angel_thumb.jpg b/data/images/paintings/thumbs/Duccio_Angel_thumb.jpg new file mode 100644 index 0000000..fbb7f66 Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_Angel_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_Apostles_Philip_and_James_the_Elder_thumb.jpg b/data/images/paintings/thumbs/Duccio_Apostles_Philip_and_James_the_Elder_thumb.jpg new file mode 100644 index 0000000..0a6b36f Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_Apostles_Philip_and_James_the_Elder_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_Disputation_with_the_Doctors_thumb.jpg b/data/images/paintings/thumbs/Duccio_Disputation_with_the_Doctors_thumb.jpg new file mode 100644 index 0000000..32b7ab6 Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_Disputation_with_the_Doctors_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_Road_to_Emmaus__top___Noli_me_tangere__bottom__thumb.jpg b/data/images/paintings/thumbs/Duccio_Road_to_Emmaus__top___Noli_me_tangere__bottom__thumb.jpg new file mode 100644 index 0000000..dafbcae Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_Road_to_Emmaus__top___Noli_me_tangere__bottom__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_Rucellai_Madonna_thumb.jpg b/data/images/paintings/thumbs/Duccio_Rucellai_Madonna_thumb.jpg new file mode 100644 index 0000000..633f852 Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_Rucellai_Madonna_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Duccio_The_Temptation_of_Christ_on_the_Mountain_thumb.jpg b/data/images/paintings/thumbs/Duccio_The_Temptation_of_Christ_on_the_Mountain_thumb.jpg new file mode 100644 index 0000000..986279d Binary files /dev/null and b/data/images/paintings/thumbs/Duccio_The_Temptation_of_Christ_on_the_Mountain_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Berlin_Street_Scene_thumb.jpg b/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Berlin_Street_Scene_thumb.jpg new file mode 100644 index 0000000..92df473 Binary files /dev/null and b/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Berlin_Street_Scene_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Potsdamer_Platz_thumb.jpg b/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Potsdamer_Platz_thumb.jpg new file mode 100644 index 0000000..92df473 Binary files /dev/null and b/data/images/paintings/thumbs/Ernst_Ludwig_Kirchner_Potsdamer_Platz_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Fra_Angelico_Christ_as_Man_of_Sorrows_Flanked_by_Saint_Mary_Magdalene_and_Saint_John_the_Evangelist__predella__centre_panel__thumb.jpg b/data/images/paintings/thumbs/Fra_Angelico_Christ_as_Man_of_Sorrows_Flanked_by_Saint_Mary_Magdalene_and_Saint_John_the_Evangelist__predella__centre_panel__thumb.jpg new file mode 100644 index 0000000..05dcc52 Binary files /dev/null and b/data/images/paintings/thumbs/Fra_Angelico_Christ_as_Man_of_Sorrows_Flanked_by_Saint_Mary_Magdalene_and_Saint_John_the_Evangelist__predella__centre_panel__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid__thumb.jpg b/data/images/paintings/thumbs/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid__thumb.jpg new file mode 100644 index 0000000..05dcc52 Binary files /dev/null and b/data/images/paintings/thumbs/Fra_Angelico_Scenes_from_the_Lives_of_the_Desert_Fathers__Thebaid__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Francis_Picabia_Dada_Movement_thumb.jpg b/data/images/paintings/thumbs/Francis_Picabia_Dada_Movement_thumb.jpg new file mode 100644 index 0000000..e683c70 Binary files /dev/null and b/data/images/paintings/thumbs/Francis_Picabia_Dada_Movement_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Georges_Braque_Man_with_a_Guitar_thumb.jpg b/data/images/paintings/thumbs/Georges_Braque_Man_with_a_Guitar_thumb.jpg new file mode 100644 index 0000000..2aaea55 Binary files /dev/null and b/data/images/paintings/thumbs/Georges_Braque_Man_with_a_Guitar_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Gino_Severini_Blue_Dancer_thumb.jpg b/data/images/paintings/thumbs/Gino_Severini_Blue_Dancer_thumb.jpg new file mode 100644 index 0000000..e5eaa9a Binary files /dev/null and b/data/images/paintings/thumbs/Gino_Severini_Blue_Dancer_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Giotto_Canonization_of_St__Francis_thumb.jpg b/data/images/paintings/thumbs/Giotto_Canonization_of_St__Francis_thumb.jpg new file mode 100644 index 0000000..3056b35 Binary files /dev/null and b/data/images/paintings/thumbs/Giotto_Canonization_of_St__Francis_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Giotto_Stigmatization_of_St_Francis_thumb.jpg b/data/images/paintings/thumbs/Giotto_Stigmatization_of_St_Francis_thumb.jpg new file mode 100644 index 0000000..b4cc4cb Binary files /dev/null and b/data/images/paintings/thumbs/Giotto_Stigmatization_of_St_Francis_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Matisse_Luxe__Calme_et_Volupt__thumb.jpg b/data/images/paintings/thumbs/Henri_Matisse_Luxe__Calme_et_Volupt__thumb.jpg new file mode 100644 index 0000000..f62060a Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Matisse_Luxe__Calme_et_Volupt__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Matisse_The_Snail_thumb.jpg b/data/images/paintings/thumbs/Henri_Matisse_The_Snail_thumb.jpg new file mode 100644 index 0000000..6a35e08 Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Matisse_The_Snail_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Privat-Livemont_Absinthe_Robette_thumb.jpg b/data/images/paintings/thumbs/Henri_Privat-Livemont_Absinthe_Robette_thumb.jpg new file mode 100644 index 0000000..9e199ca Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Privat-Livemont_Absinthe_Robette_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Privat-Livemont_Cyclamen_thumb.jpg b/data/images/paintings/thumbs/Henri_Privat-Livemont_Cyclamen_thumb.jpg new file mode 100644 index 0000000..9e199ca Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Privat-Livemont_Cyclamen_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Privat-Livemont_Diana_thumb.jpg b/data/images/paintings/thumbs/Henri_Privat-Livemont_Diana_thumb.jpg new file mode 100644 index 0000000..9e199ca Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Privat-Livemont_Diana_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Henri_Privat-Livemont_Exposition_Universelle_thumb.jpg b/data/images/paintings/thumbs/Henri_Privat-Livemont_Exposition_Universelle_thumb.jpg new file mode 100644 index 0000000..9e199ca Binary files /dev/null and b/data/images/paintings/thumbs/Henri_Privat-Livemont_Exposition_Universelle_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Hieronymus_Bosch_Christ_Mocked_thumb.jpg b/data/images/paintings/thumbs/Hieronymus_Bosch_Christ_Mocked_thumb.jpg new file mode 100644 index 0000000..b7053f2 Binary files /dev/null and b/data/images/paintings/thumbs/Hieronymus_Bosch_Christ_Mocked_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Ivan_Klyun_Suprematist_Composition_thumb.jpg b/data/images/paintings/thumbs/Ivan_Klyun_Suprematist_Composition_thumb.jpg new file mode 100644 index 0000000..e61af01 Binary files /dev/null and b/data/images/paintings/thumbs/Ivan_Klyun_Suprematist_Composition_thumb.jpg differ diff --git a/data/images/paintings/thumbs/J__M__W__Turner_Dido_Building_Carthage_thumb.jpg b/data/images/paintings/thumbs/J__M__W__Turner_Dido_Building_Carthage_thumb.jpg new file mode 100644 index 0000000..8cfeacc Binary files /dev/null and b/data/images/paintings/thumbs/J__M__W__Turner_Dido_Building_Carthage_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jackson_Pollock_Blue_Poles_thumb.jpg b/data/images/paintings/thumbs/Jackson_Pollock_Blue_Poles_thumb.jpg new file mode 100644 index 0000000..44e9299 Binary files /dev/null and b/data/images/paintings/thumbs/Jackson_Pollock_Blue_Poles_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jackson_Pollock_One__Number_31__1950_thumb.jpg b/data/images/paintings/thumbs/Jackson_Pollock_One__Number_31__1950_thumb.jpg new file mode 100644 index 0000000..c48667c Binary files /dev/null and b/data/images/paintings/thumbs/Jackson_Pollock_One__Number_31__1950_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jacques-Louis_David_Napoleon_Crossing_the_Alps_thumb.jpg b/data/images/paintings/thumbs/Jacques-Louis_David_Napoleon_Crossing_the_Alps_thumb.jpg new file mode 100644 index 0000000..3bee3fa Binary files /dev/null and b/data/images/paintings/thumbs/Jacques-Louis_David_Napoleon_Crossing_the_Alps_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women_thumb.jpg b/data/images/paintings/thumbs/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women_thumb.jpg new file mode 100644 index 0000000..55887da Binary files /dev/null and b/data/images/paintings/thumbs/Jacques-Louis_David_The_Intervention_of_the_Sabine_Women_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Adoration_of_the_Lamb_from_the_Ghent_Altarpiece_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Adoration_of_the_Lamb_from_the_Ghent_Altarpiece_thumb.jpg new file mode 100644 index 0000000..6b3de13 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Adoration_of_the_Lamb_from_the_Ghent_Altarpiece_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Angel_of_Annunciation_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Angel_of_Annunciation_thumb.jpg new file mode 100644 index 0000000..b5a691e Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Angel_of_Annunciation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Annunciation_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Annunciation_thumb.jpg new file mode 100644 index 0000000..ae13a95 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Annunciation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Arnolfini_Portrait_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Arnolfini_Portrait_thumb.jpg new file mode 100644 index 0000000..cd2082b Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Arnolfini_Portrait_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Cumaean_Sibyl_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Cumaean_Sibyl_thumb.jpg new file mode 100644 index 0000000..793032c Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Cumaean_Sibyl_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Dresden_Triptych_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Dresden_Triptych_thumb.jpg new file mode 100644 index 0000000..01aa0d6 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Dresden_Triptych_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view_thumb.jpg new file mode 100644 index 0000000..882f60c Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_and_arched_window_with_a_view_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_thumb.jpg new file mode 100644 index 0000000..22a58c3 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Erythraean_Sibyl_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Lucca_Madonna_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Lucca_Madonna_thumb.jpg new file mode 100644 index 0000000..c40a53e Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Lucca_Madonna_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele_thumb.jpg new file mode 100644 index 0000000..f649c1f Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Madonna_with_Canon_Joris_van_der_Paele_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Music-making_Angels_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Music-making_Angels_thumb.jpg new file mode 100644 index 0000000..1889731 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Music-making_Angels_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Saint_Barbara_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Saint_Barbara_thumb.jpg new file mode 100644 index 0000000..ae75226 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Saint_Barbara_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_St_Christopher_leading_the_pilgrims_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_St_Christopher_leading_the_pilgrims_thumb.jpg new file mode 100644 index 0000000..1beb766 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_St_Christopher_leading_the_pilgrims_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor_thumb.jpg new file mode 100644 index 0000000..07eba80 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__The_Donor_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed__thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed__thumb.jpg new file mode 100644 index 0000000..a1741bf Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_The_Ghent_Altarpiece__wings_closed__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_The_Just_Judges_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_The_Just_Judges_thumb.jpg new file mode 100644 index 0000000..420825d Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_The_Just_Judges_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_The_hermits_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_The_hermits_thumb.jpg new file mode 100644 index 0000000..8f8bd85 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_The_hermits_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jan_van_Eyck_Virgin_with_Female_Saints_thumb.jpg b/data/images/paintings/thumbs/Jan_van_Eyck_Virgin_with_Female_Saints_thumb.jpg new file mode 100644 index 0000000..9c20428 Binary files /dev/null and b/data/images/paintings/thumbs/Jan_van_Eyck_Virgin_with_Female_Saints_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Angelus_thumb.jpg b/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Angelus_thumb.jpg new file mode 100644 index 0000000..53366dc Binary files /dev/null and b/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Angelus_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Sower_thumb.jpg b/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Sower_thumb.jpg new file mode 100644 index 0000000..53366dc Binary files /dev/null and b/data/images/paintings/thumbs/Jean-Fran_ois_Millet_The_Sower_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes_thumb.jpg b/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes_thumb.jpg new file mode 100644 index 0000000..827c598 Binary files /dev/null and b/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Marshy_Plain__Seen_from_Above__and_Notes_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Mechanical_Military_Drum_thumb.jpg b/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Mechanical_Military_Drum_thumb.jpg new file mode 100644 index 0000000..827c598 Binary files /dev/null and b/data/images/paintings/thumbs/Leonardo_da_Vinci_A_Mechanical_Military_Drum_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Deawing_of_the_Heart_and_its_Blood_Vessels_thumb.jpg b/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Deawing_of_the_Heart_and_its_Blood_Vessels_thumb.jpg new file mode 100644 index 0000000..b31b80f Binary files /dev/null and b/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Deawing_of_the_Heart_and_its_Blood_Vessels_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels_thumb.jpg b/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels_thumb.jpg new file mode 100644 index 0000000..827c598 Binary files /dev/null and b/data/images/paintings/thumbs/Leonardo_da_Vinci_Anatomical_Drawings_of_the_Heart_and_its_Blood_Vessels_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Lyubov_Popova_Non-Objective_Composition_thumb.jpg b/data/images/paintings/thumbs/Lyubov_Popova_Non-Objective_Composition_thumb.jpg new file mode 100644 index 0000000..67f4f4e Binary files /dev/null and b/data/images/paintings/thumbs/Lyubov_Popova_Non-Objective_Composition_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Lyubov_Popova_Spatial_Force_Construction_thumb.jpg b/data/images/paintings/thumbs/Lyubov_Popova_Spatial_Force_Construction_thumb.jpg new file mode 100644 index 0000000..67f4f4e Binary files /dev/null and b/data/images/paintings/thumbs/Lyubov_Popova_Spatial_Force_Construction_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Mark_Rothko_No__10_thumb.jpg b/data/images/paintings/thumbs/Mark_Rothko_No__10_thumb.jpg new file mode 100644 index 0000000..804767c Binary files /dev/null and b/data/images/paintings/thumbs/Mark_Rothko_No__10_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Mark_Rothko_Orange_and_Yellow_thumb.jpg b/data/images/paintings/thumbs/Mark_Rothko_Orange_and_Yellow_thumb.jpg new file mode 100644 index 0000000..872fe65 Binary files /dev/null and b/data/images/paintings/thumbs/Mark_Rothko_Orange_and_Yellow_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Mark_Rothko_Untitled__Black_on_Gray__thumb.jpg b/data/images/paintings/thumbs/Mark_Rothko_Untitled__Black_on_Gray__thumb.jpg new file mode 100644 index 0000000..872fe65 Binary files /dev/null and b/data/images/paintings/thumbs/Mark_Rothko_Untitled__Black_on_Gray__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Mark_Rothko_White_Center_thumb.jpg b/data/images/paintings/thumbs/Mark_Rothko_White_Center_thumb.jpg new file mode 100644 index 0000000..872fe65 Binary files /dev/null and b/data/images/paintings/thumbs/Mark_Rothko_White_Center_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Coin_in_the_fish_s_mouth_thumb.jpg b/data/images/paintings/thumbs/Masaccio_Coin_in_the_fish_s_mouth_thumb.jpg new file mode 100644 index 0000000..9ff264d Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Coin_in_the_fish_s_mouth_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Madonna_dell_Umilt__thumb.jpg b/data/images/paintings/thumbs/Masaccio_Madonna_dell_Umilt__thumb.jpg new file mode 100644 index 0000000..1e4f3aa Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Madonna_dell_Umilt__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Raising_of_the_Son_of_Teophilus_and_St__Peter_Enthroned_thumb.jpg b/data/images/paintings/thumbs/Masaccio_Raising_of_the_Son_of_Teophilus_and_St__Peter_Enthroned_thumb.jpg new file mode 100644 index 0000000..1e4f3aa Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Raising_of_the_Son_of_Teophilus_and_St__Peter_Enthroned_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Repentance_of_Saint_Peter_thumb.jpg b/data/images/paintings/thumbs/Masaccio_Repentance_of_Saint_Peter_thumb.jpg new file mode 100644 index 0000000..1e4f3aa Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Repentance_of_Saint_Peter_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Saint_Paul_thumb.jpg b/data/images/paintings/thumbs/Masaccio_Saint_Paul_thumb.jpg new file mode 100644 index 0000000..1e4f3aa Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Saint_Paul_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_Saint_Yves_thumb.jpg b/data/images/paintings/thumbs/Masaccio_Saint_Yves_thumb.jpg new file mode 100644 index 0000000..1e4f3aa Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_Saint_Yves_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Masaccio_The_Decapitation_of_Saint_John_the_Baptist_thumb.jpg b/data/images/paintings/thumbs/Masaccio_The_Decapitation_of_Saint_John_the_Baptist_thumb.jpg new file mode 100644 index 0000000..b3d158c Binary files /dev/null and b/data/images/paintings/thumbs/Masaccio_The_Decapitation_of_Saint_John_the_Baptist_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Red_Trees_thumb.jpg b/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Red_Trees_thumb.jpg new file mode 100644 index 0000000..64f734b Binary files /dev/null and b/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Red_Trees_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Village_thumb.jpg b/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Village_thumb.jpg new file mode 100644 index 0000000..ee6b808 Binary files /dev/null and b/data/images/paintings/thumbs/Maurice_de_Vlaminck_The_Village_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Max_Ernst_Forest_and_Dove_thumb.jpg b/data/images/paintings/thumbs/Max_Ernst_Forest_and_Dove_thumb.jpg new file mode 100644 index 0000000..21ae76a Binary files /dev/null and b/data/images/paintings/thumbs/Max_Ernst_Forest_and_Dove_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_A_nude_man_and_two_figures_behind_him_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_A_nude_man_and_two_figures_behind_him_thumb.jpg new file mode 100644 index 0000000..a969343 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_A_nude_man_and_two_figures_behind_him_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Achim_and_Eliud_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Achim_and_Eliud_thumb.jpg new file mode 100644 index 0000000..3c03b5f Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Achim_and_Eliud_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Azor_and_Zadok_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Azor_and_Zadok_thumb.jpg new file mode 100644 index 0000000..3c03b5f Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Azor_and_Zadok_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Doni_Tondo_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Doni_Tondo_thumb.jpg new file mode 100644 index 0000000..9daa28d Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Doni_Tondo_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Eleazar_and_Matthan_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Eleazar_and_Matthan_thumb.jpg new file mode 100644 index 0000000..3c03b5f Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Eleazar_and_Matthan_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Erythraean_Sibyl_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Erythraean_Sibyl_thumb.jpg new file mode 100644 index 0000000..22a58c3 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Erythraean_Sibyl_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Judith_and_Holofernes_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Judith_and_Holofernes_thumb.jpg new file mode 100644 index 0000000..3f8a2ef Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Judith_and_Holofernes_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Piet__Spirituali_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Piet__Spirituali_thumb.jpg new file mode 100644 index 0000000..3c03b5f Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Piet__Spirituali_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Prophet_Daniel_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Prophet_Daniel_thumb.jpg new file mode 100644 index 0000000..27e8907 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Prophet_Daniel_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso_thumb.jpg new file mode 100644 index 0000000..e7cc878 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Studies_of_a_Horse_with_Two_Nude_Riders_and_a_Male_Torso_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto__thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto__thumb.jpg new file mode 100644 index 0000000..d80757e Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Study_for_the_Nude_Youth_over_the_Prophet_Daniel__recto__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_The_Stigmata_of_Saint_Francis_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_The_Stigmata_of_Saint_Francis_thumb.jpg new file mode 100644 index 0000000..722a415 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_The_Stigmata_of_Saint_Francis_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left_thumb.jpg new file mode 100644 index 0000000..dd4a432 Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Three_Standing_Men_in_Wide_Cloaks_Turned_to_the_Left_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right_thumb.jpg b/data/images/paintings/thumbs/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right_thumb.jpg new file mode 100644 index 0000000..d22ce1a Binary files /dev/null and b/data/images/paintings/thumbs/Michelangelo_Two_Nude_Studies_of_a_Man_Storming_Forward_and_Another_Turning_to_the_Right_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Odilon_Redon_Pandora_thumb.jpg b/data/images/paintings/thumbs/Odilon_Redon_Pandora_thumb.jpg new file mode 100644 index 0000000..ba0fd0a Binary files /dev/null and b/data/images/paintings/thumbs/Odilon_Redon_Pandora_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Pablo_Picasso_Girl_before_a_Mirror_thumb.jpg b/data/images/paintings/thumbs/Pablo_Picasso_Girl_before_a_Mirror_thumb.jpg new file mode 100644 index 0000000..939d041 Binary files /dev/null and b/data/images/paintings/thumbs/Pablo_Picasso_Girl_before_a_Mirror_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Parmigianino_Madonna_with_the_Long_Neck_thumb.jpg b/data/images/paintings/thumbs/Parmigianino_Madonna_with_the_Long_Neck_thumb.jpg new file mode 100644 index 0000000..a760238 Binary files /dev/null and b/data/images/paintings/thumbs/Parmigianino_Madonna_with_the_Long_Neck_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Paul_C_zanne_The_Large_Bathers_thumb.jpg b/data/images/paintings/thumbs/Paul_C_zanne_The_Large_Bathers_thumb.jpg new file mode 100644 index 0000000..1c6dca8 Binary files /dev/null and b/data/images/paintings/thumbs/Paul_C_zanne_The_Large_Bathers_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Paul_Gauguin_Spirit_of_the_Dead_Watching_thumb.jpg b/data/images/paintings/thumbs/Paul_Gauguin_Spirit_of_the_Dead_Watching_thumb.jpg new file mode 100644 index 0000000..531e6dd Binary files /dev/null and b/data/images/paintings/thumbs/Paul_Gauguin_Spirit_of_the_Dead_Watching_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Pontormo_Supper_at_Emmaus_thumb.jpg b/data/images/paintings/thumbs/Pontormo_Supper_at_Emmaus_thumb.jpg new file mode 100644 index 0000000..996fc0e Binary files /dev/null and b/data/images/paintings/thumbs/Pontormo_Supper_at_Emmaus_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Raphael_Half-length_portrait_of_a_young_woman_in_profile_thumb.jpg b/data/images/paintings/thumbs/Raphael_Half-length_portrait_of_a_young_woman_in_profile_thumb.jpg new file mode 100644 index 0000000..07156a9 Binary files /dev/null and b/data/images/paintings/thumbs/Raphael_Half-length_portrait_of_a_young_woman_in_profile_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Rembrandt_The_Syndics_of_the_Drapers__Guild_thumb.jpg b/data/images/paintings/thumbs/Rembrandt_The_Syndics_of_the_Drapers__Guild_thumb.jpg new file mode 100644 index 0000000..08861a5 Binary files /dev/null and b/data/images/paintings/thumbs/Rembrandt_The_Syndics_of_the_Drapers__Guild_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Ren__Magritte_Personal_Values_thumb.jpg b/data/images/paintings/thumbs/Ren__Magritte_Personal_Values_thumb.jpg new file mode 100644 index 0000000..9024d0a Binary files /dev/null and b/data/images/paintings/thumbs/Ren__Magritte_Personal_Values_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Ren__Magritte_The_Son_of_Man_thumb.jpg b/data/images/paintings/thumbs/Ren__Magritte_The_Son_of_Man_thumb.jpg new file mode 100644 index 0000000..dcbe372 Binary files /dev/null and b/data/images/paintings/thumbs/Ren__Magritte_The_Son_of_Man_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Richard_Hamilton_Hers_is_a_Luscious_Situation_thumb.jpg b/data/images/paintings/thumbs/Richard_Hamilton_Hers_is_a_Luscious_Situation_thumb.jpg new file mode 100644 index 0000000..1c5b94d Binary files /dev/null and b/data/images/paintings/thumbs/Richard_Hamilton_Hers_is_a_Luscious_Situation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Richard_Hamilton_Hommage___Chrysler_Corp__thumb.jpg b/data/images/paintings/thumbs/Richard_Hamilton_Hommage___Chrysler_Corp__thumb.jpg new file mode 100644 index 0000000..1c5b94d Binary files /dev/null and b/data/images/paintings/thumbs/Richard_Hamilton_Hommage___Chrysler_Corp__thumb.jpg differ diff --git a/data/images/paintings/thumbs/Richard_Hamilton_Swingeing_London_67_thumb.jpg b/data/images/paintings/thumbs/Richard_Hamilton_Swingeing_London_67_thumb.jpg new file mode 100644 index 0000000..b22e570 Binary files /dev/null and b/data/images/paintings/thumbs/Richard_Hamilton_Swingeing_London_67_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories_thumb.jpg b/data/images/paintings/thumbs/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories_thumb.jpg new file mode 100644 index 0000000..2e6daeb Binary files /dev/null and b/data/images/paintings/thumbs/Richard_Hamilton_Towards_a_Definitive_Statement_on_the_Coming_Trends_in_Menswear_and_Accessories_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus_thumb.jpg new file mode 100644 index 0000000..148ccd2 Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Cambridge_altar_retable__Scene__three_saints_with_angels__from_left__S__Geminianus__S__Michael__S__Augustinus_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Christ_on_the_Cross_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Christ_on_the_Cross_thumb.jpg new file mode 100644 index 0000000..fd12a18 Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Christ_on_the_Cross_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Crucifix__Christ__Mary_and_John_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Crucifix__Christ__Mary_and_John_thumb.jpg new file mode 100644 index 0000000..06fa82a Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Crucifix__Christ__Mary_and_John_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Madonna_of_Mercy_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Madonna_of_Mercy_thumb.jpg new file mode 100644 index 0000000..e9c1478 Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Madonna_of_Mercy_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Polyptych_of_Saint_Dominique_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Polyptych_of_Saint_Dominique_thumb.jpg new file mode 100644 index 0000000..39c6c7b Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Polyptych_of_Saint_Dominique_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Saint_Simon_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Saint_Simon_thumb.jpg new file mode 100644 index 0000000..85ee7d3 Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Saint_Simon_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_The_Entombment_of_Christ_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_The_Entombment_of_Christ_thumb.jpg new file mode 100644 index 0000000..6d1736b Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_The_Entombment_of_Christ_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Simone_Martini_Virgin_of_the_Annunciation_thumb.jpg b/data/images/paintings/thumbs/Simone_Martini_Virgin_of_the_Annunciation_thumb.jpg new file mode 100644 index 0000000..003f44f Binary files /dev/null and b/data/images/paintings/thumbs/Simone_Martini_Virgin_of_the_Annunciation_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Theophanes_the_Greek_Theotokos_of_Tolga_thumb.jpg b/data/images/paintings/thumbs/Theophanes_the_Greek_Theotokos_of_Tolga_thumb.jpg new file mode 100644 index 0000000..2c7379d Binary files /dev/null and b/data/images/paintings/thumbs/Theophanes_the_Greek_Theotokos_of_Tolga_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Christ_thumb.jpg b/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Christ_thumb.jpg new file mode 100644 index 0000000..0680bda Binary files /dev/null and b/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Christ_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Jesus_thumb.jpg b/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Jesus_thumb.jpg new file mode 100644 index 0000000..0680bda Binary files /dev/null and b/data/images/paintings/thumbs/Theophanes_the_Greek_Transfiguration_of_Jesus_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape_thumb.jpg b/data/images/paintings/thumbs/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape_thumb.jpg index d205465..a0bfba2 100644 Binary files a/data/images/paintings/thumbs/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape_thumb.jpg and b/data/images/paintings/thumbs/Titian_An_Idyll___A_Mother_and_a_Halberdier_in_a_Wooded_Landscape_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Titian_Lochis_Madonna_thumb.jpg b/data/images/paintings/thumbs/Titian_Lochis_Madonna_thumb.jpg new file mode 100644 index 0000000..d205465 Binary files /dev/null and b/data/images/paintings/thumbs/Titian_Lochis_Madonna_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr_thumb.jpg b/data/images/paintings/thumbs/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr_thumb.jpg new file mode 100644 index 0000000..0470d07 Binary files /dev/null and b/data/images/paintings/thumbs/Titian_Studies_for_Martyrdom_of_Saint_Peter_the_Martyr_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Umberto_Boccioni_States_of_Mind_I__The_Farewells_thumb.jpg b/data/images/paintings/thumbs/Umberto_Boccioni_States_of_Mind_I__The_Farewells_thumb.jpg new file mode 100644 index 0000000..e33a27e Binary files /dev/null and b/data/images/paintings/thumbs/Umberto_Boccioni_States_of_Mind_I__The_Farewells_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Umberto_Boccioni_The_Street_Enters_the_House_thumb.jpg b/data/images/paintings/thumbs/Umberto_Boccioni_The_Street_Enters_the_House_thumb.jpg new file mode 100644 index 0000000..5ff3e08 Binary files /dev/null and b/data/images/paintings/thumbs/Umberto_Boccioni_The_Street_Enters_the_House_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Vincent_van_Gogh_Wheatfield_with_Crows_thumb.jpg b/data/images/paintings/thumbs/Vincent_van_Gogh_Wheatfield_with_Crows_thumb.jpg new file mode 100644 index 0000000..bc5d6cf Binary files /dev/null and b/data/images/paintings/thumbs/Vincent_van_Gogh_Wheatfield_with_Crows_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Wassily_Kandinsky_Improvisation_28_thumb.jpg b/data/images/paintings/thumbs/Wassily_Kandinsky_Improvisation_28_thumb.jpg new file mode 100644 index 0000000..4adbfda Binary files /dev/null and b/data/images/paintings/thumbs/Wassily_Kandinsky_Improvisation_28_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Willem_de_Kooning_Woman_I_thumb.jpg b/data/images/paintings/thumbs/Willem_de_Kooning_Woman_I_thumb.jpg new file mode 100644 index 0000000..227c670 Binary files /dev/null and b/data/images/paintings/thumbs/Willem_de_Kooning_Woman_I_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Zeuxis_Centaur_Family_thumb.jpg b/data/images/paintings/thumbs/Zeuxis_Centaur_Family_thumb.jpg new file mode 100644 index 0000000..76e2333 Binary files /dev/null and b/data/images/paintings/thumbs/Zeuxis_Centaur_Family_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Zeuxis_Helen_of_Troy_thumb.jpg b/data/images/paintings/thumbs/Zeuxis_Helen_of_Troy_thumb.jpg new file mode 100644 index 0000000..2f3a132 Binary files /dev/null and b/data/images/paintings/thumbs/Zeuxis_Helen_of_Troy_thumb.jpg differ diff --git a/data/images/paintings/thumbs/_douard_Manet_A_Bar_at_the_Folies-Berg_re_thumb.jpg b/data/images/paintings/thumbs/_douard_Manet_A_Bar_at_the_Folies-Berg_re_thumb.jpg new file mode 100644 index 0000000..5a0c24b Binary files /dev/null and b/data/images/paintings/thumbs/_douard_Manet_A_Bar_at_the_Folies-Berg_re_thumb.jpg differ diff --git a/data/images/paintings/thumbs/_douard_Manet_Luncheon_on_the_Grass_thumb.jpg b/data/images/paintings/thumbs/_douard_Manet_Luncheon_on_the_Grass_thumb.jpg new file mode 100644 index 0000000..953c8e5 Binary files /dev/null and b/data/images/paintings/thumbs/_douard_Manet_Luncheon_on_the_Grass_thumb.jpg differ diff --git a/data/images/paintings/thumbs/_douard_Manet_Olympia_thumb.jpg b/data/images/paintings/thumbs/_douard_Manet_Olympia_thumb.jpg new file mode 100644 index 0000000..e100bcc Binary files /dev/null and b/data/images/paintings/thumbs/_douard_Manet_Olympia_thumb.jpg differ diff --git a/data/images/paintings/thumbs/_douard_Manet_The_Balcony_thumb.jpg b/data/images/paintings/thumbs/_douard_Manet_The_Balcony_thumb.jpg new file mode 100644 index 0000000..6a68f0b Binary files /dev/null and b/data/images/paintings/thumbs/_douard_Manet_The_Balcony_thumb.jpg differ diff --git a/data/images/paintings/thumbs/_douard_Manet_The_Dead_Toreador_thumb.jpg b/data/images/paintings/thumbs/_douard_Manet_The_Dead_Toreador_thumb.jpg new file mode 100644 index 0000000..5a0c24b Binary files /dev/null and b/data/images/paintings/thumbs/_douard_Manet_The_Dead_Toreador_thumb.jpg differ diff --git a/db/migrate-influence-sources.sql b/db/migrate-influence-sources.sql new file mode 100644 index 0000000..24f7fbb --- /dev/null +++ b/db/migrate-influence-sources.sql @@ -0,0 +1,70 @@ +-- Polymorphic influence sources: painting, artist, or movement → a painting +CREATE TABLE IF NOT EXISTS painting_influence_sources ( + id SERIAL PRIMARY KEY, + painting_id INTEGER NOT NULL REFERENCES paintings(id) ON DELETE CASCADE, + source_type VARCHAR(20) NOT NULL CHECK (source_type IN ('painting', 'artist', 'movement')), + source_painting_id INTEGER REFERENCES paintings(id) ON DELETE CASCADE, + source_artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE, + source_movement_id INTEGER REFERENCES art_movements(id) ON DELETE CASCADE, + period_note VARCHAR(240), + period_start_year INTEGER, + period_end_year INTEGER, + notes TEXT, + source VARCHAR(500), + aspects TEXT, + quote TEXT, + source_author VARCHAR(200), + source_url VARCHAR(500), + discovered_via VARCHAR(120), + confidence VARCHAR(20) NOT NULL DEFAULT 'curated', + CONSTRAINT painting_influence_sources_source_shape CHECK ( + (source_type = 'painting' AND source_painting_id IS NOT NULL AND source_artist_id IS NULL AND source_movement_id IS NULL) + OR (source_type = 'artist' AND source_artist_id IS NOT NULL AND source_painting_id IS NULL AND source_movement_id IS NULL) + OR (source_type = 'movement' AND source_movement_id IS NOT NULL AND source_painting_id IS NULL AND source_artist_id IS NULL) + ) +); + +CREATE UNIQUE INDEX IF NOT EXISTS painting_influence_sources_unique + ON painting_influence_sources ( + painting_id, + source_type, + COALESCE(source_painting_id, 0), + COALESCE(source_artist_id, 0), + COALESCE(source_movement_id, 0) + ); + +CREATE INDEX IF NOT EXISTS painting_influence_sources_painting_idx + ON painting_influence_sources (painting_id); + +CREATE INDEX IF NOT EXISTS painting_influence_sources_artist_idx + ON painting_influence_sources (source_artist_id); + +CREATE INDEX IF NOT EXISTS painting_influence_sources_movement_idx + ON painting_influence_sources (source_movement_id); + +-- Backfill legacy painting-to-painting edges +INSERT INTO painting_influence_sources ( + painting_id, + source_type, + source_painting_id, + notes, + source, + aspects, + quote, + source_author, + source_url, + confidence +) +SELECT + pi.painting_id, + 'painting', + pi.influenced_by_painting_id, + pi.notes, + pi.source, + pi.aspects, + pi.quote, + pi.source_author, + pi.source_url, + 'curated' +FROM painting_influences pi +ON CONFLICT DO NOTHING; diff --git a/package.json b/package.json index 7ba8117..6c994d0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "audit-painting-images": "node scripts/audit-painting-images.js", "expand-catalog": "node scripts/expand-paintings.js", "update-influences": "node scripts/update-influences.js", + "migrate:influence-sources": "node scripts/migrate-influence-sources.js", + "discover-influences": "node scripts/update-influences.js --discover-only", "server": "node server/index.js", "dev:server": "nodemon server/index.js", "dev:client": "npm run dev --prefix client", diff --git a/scripts/art-influences-data.js b/scripts/art-influences-data.js index a012c17..bd4d06a 100644 --- a/scripts/art-influences-data.js +++ b/scripts/art-influences-data.js @@ -409,10 +409,23 @@ module.exports = [ source: 'The Story of Art', }, { - work: { artist: 'Pablo Picasso', title: 'Les Demoiselles d\'Avignon' }, - influencedBy: { artist: 'Henri Matisse', title: 'The Dance' }, - notes: 'Picasso\'s Demoiselles responded to Matisse\'s Fauve primitivism with Iberian and African formal violence.', - aspects: 'primitivism, mask-like faces, color, rivalry', + work: { artist: 'Pablo Picasso', title: 'Les Demoiselles d\'Avignon', year: 1907 }, + influencedBy: [ + { type: 'painting', artist: 'Henri Matisse', title: 'The Dance' }, + { type: 'painting', artist: 'Paul Cézanne', title: 'The Bathers' }, + { + type: 'artist', + artist: 'Paul Cézanne', + period: { duringCreation: true }, + }, + { + type: 'movement', + movement: 'Fauvism', + period: { start: 1905, end: 1907, note: 'Paris avant-garde rivalry with Matisse' }, + }, + ], + notes: 'Picasso\'s Demoiselles responded to Fauve primitivism while absorbing Cézanne\'s structural bathers and Iberian/African mask forms.', + aspects: 'primitivism, mask-like faces, structure, rivalry', source_author: 'E. H. Gombrich', source: 'The Story of Art', }, diff --git a/scripts/image-fetcher.js b/scripts/image-fetcher.js index 4be1371..0928588 100644 --- a/scripts/image-fetcher.js +++ b/scripts/image-fetcher.js @@ -1173,14 +1173,199 @@ async function resolveImageUrl(wikiTitle, options = {}) { return { url: pair.fullUrl, source: pair.source }; } -async function downloadImageToFile(url, destPath) { - if (fs.existsSync(destPath)) return destPath; +async function downloadImageToFile(url, destPath, { force = false } = {}) { + if (!force && fs.existsSync(destPath)) return destPath; await throttle(); const buffer = await withRetry(() => fetchBuffer(url)); + fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.writeFileSync(destPath, buffer); return destPath; } +function decodeGoogleEmbeddedUrl(raw) { + return raw + .replace(/\\u003d/g, '=') + .replace(/\\u0026/g, '&') + .replace(/\\u0027/g, "'") + .replace(/\\\//g, '/'); +} + +function isLikelyImageUrl(url) { + if (!url || !/^https?:\/\//i.test(url)) return false; + if (/^data:/i.test(url)) return false; + if (/gstatic\.com\/images\?/i.test(url)) return false; + return true; +} + +function pickBestGoogleImageUrl(candidates) { + const unique = [...new Set(candidates.filter(isLikelyImageUrl))]; + const scored = unique.map((url) => { + let score = 0; + if (/upload\.wikimedia\.org/i.test(url)) score += 40; + if (/googleusercontent\.com/i.test(url)) score += 30; + if (/\.(jpe?g|png|webp)(\?|$)/i.test(url)) score += 20; + if (/thumb|thumbnail|small|icon|logo|avatar/i.test(url)) score -= 25; + if (/=\s*[sS]\d{2,3}(-c|-rw)?(\||$)/.test(url) || /[?&]w=\d{2,3}(&|$)/.test(url)) score -= 15; + return { url, score }; + }); + scored.sort((a, b) => b.score - a.score); + return scored[0]?.url || unique[0] || null; +} + +function extractGoogleImageCandidates(html) { + const candidates = []; + + for (const match of html.matchAll(/"ou":"((?:https?:\\\/\\\/|\\\/\\\/)[^"\\]+)"/g)) { + candidates.push(decodeGoogleEmbeddedUrl(match[1])); + } + + for (const match of html.matchAll(/"ou":\s*"((?:https?:\/\/)[^"]+)"/g)) { + candidates.push(match[1]); + } + + for (const match of html.matchAll(/\["(https:\\\/\\\/[^"]+)",(?:\d+,\d+|\d+)\]/g)) { + candidates.push(decodeGoogleEmbeddedUrl(match[1])); + } + + for (const match of html.matchAll(/imgurl=([^&"]+)/g)) { + try { + candidates.push(decodeURIComponent(match[1])); + } catch { + // skip + } + } + + return candidates; +} + +async function searchGoogleCustomSearchImagesFirst(query) { + const apiKey = process.env.GOOGLE_CSE_API_KEY; + const cx = process.env.GOOGLE_CSE_CX; + if (!apiKey || !cx) return null; + + const url = + `https://www.googleapis.com/customsearch/v1?key=${encodeURIComponent(apiKey)}` + + `&cx=${encodeURIComponent(cx)}&q=${encodeURIComponent(query)}&searchType=image&num=1&safe=active`; + const data = await fetchJson(url); + const item = data?.items?.[0]; + if (!item?.link) return null; + return { + imageUrl: item.link, + source: 'google-custom-search', + sourceLabel: 'Google Custom Search', + }; +} + +async function searchDuckDuckGoImagesFirst(query) { + const html = await fetchHtml( + `https://duckduckgo.com/?q=${encodeURIComponent(query)}&iar=images&iax=images&ia=images`, + { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + } + ); + const vqd = + html.match(/vqd=['"]([^'"]+)['"]/)?.[1] || + html.match(/vqd=([\d-]+)/)?.[1] || + null; + if (!vqd) return null; + + await throttle(); + const jsUrl = `https://duckduckgo.com/i.js?o=json&q=${encodeURIComponent(query)}&l=us-en&vqd=${encodeURIComponent(vqd)}&f=,,,&p=1`; + const payload = await new Promise((resolve, reject) => { + https + .get( + jsUrl, + { headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' } }, + (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (err) { + reject(err); + } + }); + } + ) + .on('error', reject); + }); + + const first = payload?.results?.[0]; + const imageUrl = first?.image || first?.thumbnail; + if (!imageUrl) return null; + return { + imageUrl, + source: 'duckduckgo-images', + sourceLabel: 'DuckDuckGo Images (fallback)', + }; +} + +/** First Google-family image result for artist + painting title (developer image audit). */ +async function searchGoogleImagesFirst(artistName, paintingTitle) { + const simple = simplifyPaintingTitle(paintingTitle); + const query = `${artistName} ${simple} painting`.trim(); + const searchUrl = + `https://www.google.com/search?q=${encodeURIComponent(query)}&tbm=isch&hl=en&ijn=0`; + + const custom = await searchGoogleCustomSearchImagesFirst(query).catch(() => null); + if (custom?.imageUrl) { + return { query, searchUrl, ...custom }; + } + + const gac = await getGoogleArtsCultureImages(simple, artistName).catch(() => null); + if (gac?.fullUrl) { + return { + query, + imageUrl: gac.fullUrl, + thumbUrl: gac.thumbUrl, + searchUrl: `https://artsandculture.google.com/search?q=${encodeURIComponent(query)}`, + source: 'google-arts-culture', + sourceLabel: 'Google Arts & Culture', + }; + } + + try { + const html = await fetchHtml(searchUrl, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'text/html,application/xhtml+xml', + 'Accept-Language': 'en-US,en;q=0.9', + }, + }); + const imageUrl = pickBestGoogleImageUrl(extractGoogleImageCandidates(html)); + if (imageUrl) { + return { + query, + imageUrl, + searchUrl, + source: 'google-images', + sourceLabel: 'Google Images', + }; + } + } catch { + // continue to fallback + } + + const ddg = await searchDuckDuckGoImagesFirst(query).catch(() => null); + if (ddg?.imageUrl) { + return { query, searchUrl, ...ddg }; + } + + return { query, imageUrl: null, searchUrl, source: 'google-images', sourceLabel: 'Google Images' }; +} + +async function fetchImageBuffer(url) { + await throttle(); + return withRetry(() => fetchBuffer(url)); +} + async function generateThumbnailFromFull(fullDest, thumbDest, width = THUMB_WIDTH) { const sharp = require('sharp'); if (!fs.existsSync(fullDest)) return false; @@ -1311,8 +1496,12 @@ module.exports = { generateThumbnailFromFull, searchWikipediaTitle, searchWebForPaintingImages, + searchGoogleImagesFirst, + fetchImageBuffer, + pickExt, getGoogleArtsCultureImages, simplifyPaintingTitle, + fetchHtml, DEFAULT_BATCH_MAX_WAIT_MS, FetchDeadline, sleep, diff --git a/scripts/influence-discovery.js b/scripts/influence-discovery.js new file mode 100644 index 0000000..fda114d --- /dev/null +++ b/scripts/influence-discovery.js @@ -0,0 +1,320 @@ +/** + * Discover painting influence candidates from art-history web sources. + * Returns normalized source refs for painting_influence_sources (artist / movement / painting). + */ +const { fetchHtml, searchWikipediaTitle, sleep } = require('./image-fetcher'); +const { normalizeArtist } = require('./influence-resolver'); + +const SEARCH_SITES = [ + 'theartstory.org', + 'metmuseum.org', + 'artsandculture.google.com', + 'nga.gov', + 'artic.edu', + 'moma.org', + 'britannica.com', + 'jstor.org', + 'oxfordartonline.com', + 'wikiart.org', + 'wikipedia.org', +]; + +const MOVEMENT_HINTS = [ + 'Impressionism', + 'Post-Impressionism', + 'Cubism', + 'Expressionism', + 'Surrealism', + 'Baroque', + 'Renaissance', + 'Romanticism', + 'Realism', + 'Symbolism', + 'Fauvism', + 'Abstract Expressionism', + 'Pop Art', + 'Neoclassicism', + 'Rococo', + 'Mannerism', + 'Gothic', + 'Byzantine', + 'Art Nouveau', + 'Futurism', + 'Dada', + 'Minimalism', +]; + +function uniqueByKey(items, keyFn) { + const seen = new Set(); + return items.filter((item) => { + const key = keyFn(item); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function stripHtml(text) { + return (text || '') + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/\s+/g, ' ') + .trim(); +} + +function extractInfluenceNames(text) { + if (!text) return []; + const names = []; + const patterns = [ + /influenced by ([^.;\n]{4,120})/gi, + /draw(?:s|n)? on ([^.;\n]{4,120})/gi, + /indebted to ([^.;\n]{4,120})/gi, + /following the example of ([^.;\n]{4,120})/gi, + ]; + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(text)) !== null) { + const chunk = stripHtml(match[1]); + chunk + .split(/,| and |;|\//) + .map((s) => s.trim()) + .filter((s) => s.length > 3 && s.length < 80) + .forEach((s) => names.push(s)); + } + } + return names; +} + +function detectMovements(text) { + const lower = (text || '').toLowerCase(); + return MOVEMENT_HINTS.filter((m) => lower.includes(m.toLowerCase())); +} + +async function fetchJson(url) { + const res = await fetch(url, { + headers: { 'User-Agent': 'GalleryInfluenceBot/1.0 (personal art history project)' }, + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +async function discoverFromWikipedia(artistName, paintingTitle) { + const results = []; + const wikiTitle = await searchWikipediaTitle(paintingTitle, artistName); + if (!wikiTitle) return results; + + try { + const parseUrl = + `https://en.wikipedia.org/w/api.php?action=parse&page=${encodeURIComponent(wikiTitle)}` + + '&prop=wikitext&format=json&origin=*'; + const data = await fetchJson(parseUrl); + const wikitext = data?.parse?.wikitext?.['*'] || ''; + const plain = wikitext + .replace(/\{\{[^{}]*\}\}/g, ' ') + .replace(/\[\[Category:[^\]]+\]\]/gi, ' ') + .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2') + .replace(/\[\[([^\]]+)\]\]/g, '$1') + .replace(/''+/g, ''); + + for (const name of extractInfluenceNames(plain)) { + results.push({ + type: 'artist', + artist: name, + period: { duringCreation: true }, + notes: `Mentioned on Wikipedia article for "${wikiTitle}".`, + source: 'Wikipedia', + source_url: `https://en.wikipedia.org/wiki/${encodeURIComponent(wikiTitle.replace(/ /g, '_'))}`, + discovered_via: 'wikipedia', + confidence: 'discovered', + }); + } + + for (const movement of detectMovements(plain)) { + results.push({ + type: 'movement', + movement, + period: { duringCreation: true }, + notes: `Movement referenced on Wikipedia article for "${wikiTitle}".`, + source: 'Wikipedia', + source_url: `https://en.wikipedia.org/wiki/${encodeURIComponent(wikiTitle.replace(/ /g, '_'))}`, + discovered_via: 'wikipedia', + confidence: 'discovered', + }); + } + } catch { + // skip + } + + return results; +} + +async function discoverFromWikidata(artistName) { + const results = []; + try { + const searchUrl = + 'https://www.wikidata.org/w/api.php?action=wbsearchentities&format=json&language=en&type=item&limit=1' + + `&search=${encodeURIComponent(artistName)}`; + const search = await fetchJson(searchUrl); + const entityId = search?.search?.[0]?.id; + if (!entityId) return results; + + const entityUrl = `https://www.wikidata.org/wiki/Special:EntityData/${entityId}.json`; + const entityData = await fetchJson(entityUrl); + const entity = entityData?.entities?.[entityId]; + const claims = entity?.claims?.P737 || []; + + for (const claim of claims.slice(0, 8)) { + const influencedId = claim?.mainsnak?.datavalue?.value?.id; + if (!influencedId) continue; + const labelUrl = + `https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${influencedId}` + + '&props=labels&languages=en&format=json'; + const labelData = await fetchJson(labelUrl); + const label = labelData?.entities?.[influencedId]?.labels?.en?.value; + if (!label) continue; + results.push({ + type: 'artist', + artist: label, + period: { duringCreation: true }, + notes: `Wikidata P737 (influenced by) on ${artistName}.`, + source: 'Wikidata', + source_url: `https://www.wikidata.org/wiki/${entityId}`, + discovered_via: 'wikidata', + confidence: 'discovered', + }); + } + } catch { + // skip + } + return results; +} + +async function discoverFromWebSearch(artistName, paintingTitle) { + const results = []; + const query = `"${artistName}" "${paintingTitle}" influenced by ${SEARCH_SITES.map((s) => `site:${s}`).join(' OR ')}`; + + try { + const html = await fetchHtml('https://html.duckduckgo.com/html/', { + method: 'POST', + body: `q=${encodeURIComponent(query)}`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + + const links = [...html.matchAll(/uddg=([^&"]+)/g)] + .map((m) => decodeURIComponent(m[1])) + .filter((url) => SEARCH_SITES.some((site) => url.includes(site))) + .slice(0, 5); + + for (const url of links) { + try { + const page = await fetchHtml(url, { + headers: { Referer: 'https://html.duckduckgo.com/' }, + }); + const text = stripHtml(page); + const site = SEARCH_SITES.find((s) => url.includes(s)) || 'web'; + + for (const name of extractInfluenceNames(text).slice(0, 4)) { + results.push({ + type: 'artist', + artist: name, + period: { duringCreation: true }, + notes: `Found via art-history web search (${site}).`, + source: site, + source_url: url, + discovered_via: `web:${site}`, + confidence: 'discovered', + }); + } + + for (const movement of detectMovements(text).slice(0, 3)) { + results.push({ + type: 'movement', + movement, + period: { duringCreation: true }, + notes: `Movement cited on ${site}.`, + source: site, + source_url: url, + discovered_via: `web:${site}`, + confidence: 'discovered', + }); + } + + await sleep(1200); + } catch { + // next result + } + } + } catch { + // skip + } + + return results; +} + +async function discoverFromMet(artistName, paintingTitle) { + const results = []; + try { + const searchUrl = + `https://collectionapi.metmuseum.org/public/collection/v1/search?q=${encodeURIComponent( + `${artistName} ${paintingTitle}` + )}&hasImages=true`; + const search = await fetchJson(searchUrl); + const objectId = search?.objectIDs?.[0]; + if (!objectId) return results; + + const object = await fetchJson( + `https://collectionapi.metmuseum.org/public/collection/v1/objects/${objectId}` + ); + const text = [object.creditLine, object.culture, object.period, object.classification, object.medium] + .filter(Boolean) + .join('. '); + + for (const movement of detectMovements(text)) { + results.push({ + type: 'movement', + movement, + period: { duringCreation: true }, + notes: `Met Museum object metadata for ${object.title || paintingTitle}.`, + source: 'The Metropolitan Museum of Art', + source_url: object.objectURL || `https://www.metmuseum.org/art/collection/search/${objectId}`, + discovered_via: 'metmuseum', + confidence: 'discovered', + }); + } + } catch { + // skip + } + return results; +} + +async function discoverInfluencesForWork({ artist, title, year }) { + const artistName = normalizeArtist(artist); + const chunks = await Promise.all([ + discoverFromWikipedia(artistName, title), + discoverFromWikidata(artistName), + discoverFromMet(artistName, title), + discoverFromWebSearch(artistName, title), + ]); + + return uniqueByKey(chunks.flat(), (item) => + item.type === 'movement' + ? `movement:${item.movement}` + : item.type === 'artist' + ? `artist:${item.artist}` + : `painting:${item.artist}:${item.title}` + ).map((item) => ({ + ...item, + period: item.period || (year ? { duringCreation: true } : undefined), + })); +} + +module.exports = { + discoverInfluencesForWork, + discoverFromWikipedia, + discoverFromWikidata, + discoverFromWebSearch, + discoverFromMet, + SEARCH_SITES, +}; diff --git a/scripts/influence-resolver.js b/scripts/influence-resolver.js new file mode 100644 index 0000000..244eee6 --- /dev/null +++ b/scripts/influence-resolver.js @@ -0,0 +1,307 @@ +const ARTIST_ALIASES = { + 'Camille Corot': 'Jean-Baptiste-Camille Corot', +}; + +function normalizeTitle(s) { + return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); +} + +function normalizeArtist(name) { + return ARTIST_ALIASES[name] || name; +} + +function scoreTitleMatch(dbTitle, hint) { + const normDb = normalizeTitle(dbTitle); + const keywords = normalizeTitle(hint).split(' ').filter((w) => w.length > 2); + if (!keywords.length) return 0; + const hits = keywords.filter((k) => normDb.includes(k)).length; + const required = keywords.length === 1 ? 1 : Math.min(2, keywords.length); + return hits >= required ? hits : 0; +} + +function normalizeInfluencedBy(ref) { + if (!ref) return []; + if (Array.isArray(ref)) return ref; + return [{ type: 'painting', ...ref }]; +} + +function periodFields(ref, workYear) { + const period = ref.period || {}; + let start = period.start ?? period.startYear ?? null; + let end = period.end ?? period.endYear ?? null; + let note = period.note || null; + + if (period.duringCreation && workYear != null) { + start = start ?? workYear - 3; + end = end ?? workYear + 1; + note = note || `around the creation of the work (${workYear})`; + } + + return { period_start_year: start, period_end_year: end, period_note: note }; +} + +async function loadMovements(pool) { + const { rows } = await pool.query('SELECT id, name FROM art_movements'); + return new Map(rows.map((r) => [r.name.toLowerCase(), r.id])); +} + +async function findArtist(pool, name) { + const canonical = normalizeArtist(name); + const { rows } = await pool.query( + `SELECT id, name FROM artists WHERE name = $1 OR name ILIKE $2 LIMIT 1`, + [canonical, canonical] + ); + return rows[0] || null; +} + +async function ensureArtist(pool, ref, movementsByName) { + const name = normalizeArtist(ref.artist); + let artist = await findArtist(pool, name); + if (artist) return artist.id; + + const meta = ref.artistMeta; + if (!meta) return null; + + const movementId = meta.movement + ? movementsByName.get(meta.movement.toLowerCase()) || null + : null; + const century = + meta.century || (meta.birth_year ? Math.floor(meta.birth_year / 100) * 100 : null); + + const insert = await pool.query( + `INSERT INTO artists (name, birth_year, death_year, movement_id, wikipedia_title, century, portrait_path) + VALUES ($1, $2, $3, $4, $5, $6, NULL) + RETURNING id, name`, + [ + name, + meta.birth_year ?? null, + meta.death_year ?? null, + movementId, + meta.wikipedia_title || name, + century, + ] + ); + console.log(`+ artist: ${name}`); + return insert.rows[0].id; +} + +async function findPainting(pool, artistId, titleHint) { + const { rows } = await pool.query( + 'SELECT id, title, year FROM paintings WHERE artist_id = $1', + [artistId] + ); + let best = null; + let bestScore = 0; + for (const row of rows) { + const score = scoreTitleMatch(row.title, titleHint); + if (score > bestScore) { + bestScore = score; + best = row; + } + } + return best; +} + +async function ensurePainting(pool, artistId, ref, artistName, fetchImages, imageDir, savePaintingImages) { + const existing = await findPainting(pool, artistId, ref.title); + if (existing) return existing; + + const sortRes = await pool.query( + 'SELECT COALESCE(MAX(sort_order), 0) + 1 AS next FROM paintings WHERE artist_id = $1', + [artistId] + ); + const wikiTitle = ref.wikipedia_title || ref.title; + const insert = await pool.query( + `INSERT INTO paintings (artist_id, title, year, wikipedia_title, sort_order) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [artistId, ref.title, ref.year ?? null, wikiTitle, sortRes.rows[0].next] + ); + console.log(`+ painting: ${artistName} — ${ref.title}`); + + if (fetchImages && savePaintingImages) { + try { + const base = `${artistName}_${ref.title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); + const saved = await savePaintingImages(wikiTitle, base, imageDir, { + artistName, + paintingTitle: ref.title, + }); + if (saved.imagePath || saved.thumbnailPath) { + await pool.query( + `UPDATE paintings SET image_path = $1, thumbnail_path = $2 WHERE id = $3`, + [saved.imagePath, saved.thumbnailPath, insert.rows[0].id] + ); + } + } catch (err) { + console.warn(` image fetch failed: ${artistName} — ${ref.title}: ${err.message}`); + } + } + + return insert.rows[0]; +} + +async function resolveMovement(pool, movementName, movementsByName) { + if (!movementName) return null; + const id = movementsByName.get(movementName.toLowerCase()); + if (id) return id; + const { rows } = await pool.query( + `SELECT id FROM art_movements WHERE name ILIKE $1 LIMIT 1`, + [movementName] + ); + return rows[0]?.id ?? null; +} + +async function resolvePaintingRef(pool, ref, movementsByName, fetchImages, imageDir, savePaintingImages) { + const artistName = normalizeArtist(ref.artist); + const artistId = await ensureArtist(pool, ref, movementsByName); + if (!artistId) return null; + return ensurePainting(pool, artistId, ref, artistName, fetchImages, imageDir, savePaintingImages); +} + +async function resolveWorkRef(pool, workRef, movementsByName, fetchImages, imageDir, savePaintingImages) { + const artistName = normalizeArtist(workRef.artist); + const artistId = await ensureArtist(pool, workRef, movementsByName); + if (!artistId) return null; + const painting = await ensurePainting( + pool, + artistId, + workRef, + artistName, + fetchImages, + imageDir, + savePaintingImages + ); + if (!painting) return null; + return { id: painting.id, year: painting.year ?? workRef.year ?? null, artist_id: artistId }; +} + +async function insertLegacyPaintingEdge(pool, workId, sourcePaintingId, edge) { + await pool.query( + `INSERT INTO painting_influences + (painting_id, influenced_by_painting_id, notes, source, aspects, quote, source_author, source_url) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (painting_id, influenced_by_painting_id) DO NOTHING`, + [ + workId, + sourcePaintingId, + edge.notes || null, + edge.source || null, + edge.aspects || null, + edge.quote || null, + edge.source_author || null, + edge.source_url || null, + ] + ); +} + +async function insertInfluenceSource(pool, workId, sourceType, sourceIds, edge, workYear) { + const period = periodFields(edge, workYear); + const result = await pool.query( + `INSERT INTO painting_influence_sources ( + painting_id, source_type, source_painting_id, source_artist_id, source_movement_id, + period_note, period_start_year, period_end_year, + notes, source, aspects, quote, source_author, source_url, discovered_via, confidence + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) + ON CONFLICT DO NOTHING + RETURNING id`, + [ + workId, + sourceType, + sourceIds.source_painting_id ?? null, + sourceIds.source_artist_id ?? null, + sourceIds.source_movement_id ?? null, + period.period_note, + period.period_start_year, + period.period_end_year, + edge.notes || null, + edge.source || null, + edge.aspects || null, + edge.quote || null, + edge.source_author || null, + edge.source_url || null, + edge.discovered_via || null, + edge.confidence || 'curated', + ] + ); + return result.rowCount > 0; +} + +async function resolveAndInsertSource( + pool, + work, + sourceRef, + edge, + movementsByName, + fetchImages, + imageDir, + savePaintingImages +) { + const type = (sourceRef.type || 'painting').toLowerCase(); + const mergedEdge = { ...edge, ...sourceRef, type }; + + if (type === 'painting') { + const sourcePainting = await resolvePaintingRef( + pool, + sourceRef, + movementsByName, + fetchImages, + imageDir, + savePaintingImages + ); + if (!sourcePainting) return { ok: false, reason: 'unresolved painting' }; + if (sourcePainting.id === work.id) return { ok: false, reason: 'self' }; + await insertLegacyPaintingEdge(pool, work.id, sourcePainting.id, mergedEdge); + const inserted = await insertInfluenceSource( + pool, + work.id, + 'painting', + { source_painting_id: sourcePainting.id }, + mergedEdge, + work.year + ); + return { ok: true, inserted, label: `${sourceRef.artist} / ${sourceRef.title}` }; + } + + if (type === 'artist') { + const artistId = await ensureArtist(pool, sourceRef, movementsByName); + if (!artistId) return { ok: false, reason: 'unresolved artist' }; + if (artistId === work.artist_id) return { ok: false, reason: 'self artist' }; + const inserted = await insertInfluenceSource( + pool, + work.id, + 'artist', + { source_artist_id: artistId }, + mergedEdge, + work.year + ); + return { ok: true, inserted, label: sourceRef.artist }; + } + + if (type === 'movement') { + const movementId = await resolveMovement(pool, sourceRef.movement, movementsByName); + if (!movementId) return { ok: false, reason: `unresolved movement: ${sourceRef.movement}` }; + const inserted = await insertInfluenceSource( + pool, + work.id, + 'movement', + { source_movement_id: movementId }, + mergedEdge, + work.year + ); + return { ok: true, inserted, label: sourceRef.movement }; + } + + return { ok: false, reason: `unknown type: ${type}` }; +} + +module.exports = { + normalizeArtist, + normalizeInfluencedBy, + loadMovements, + resolveWorkRef, + resolveAndInsertSource, + findArtist, + findPainting, + resolveMovement, + periodFields, +}; diff --git a/scripts/migrate-influence-sources.js b/scripts/migrate-influence-sources.js new file mode 100644 index 0000000..c58da81 --- /dev/null +++ b/scripts/migrate-influence-sources.js @@ -0,0 +1,18 @@ +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); +const pool = require('../server/db'); + +async function main() { + const sqlPath = path.join(__dirname, '../db/migrate-influence-sources.sql'); + const sql = fs.readFileSync(sqlPath, 'utf8'); + await pool.query(sql); + const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM painting_influence_sources'); + console.log(`painting_influence_sources ready (${rows[0].n} rows)`); + await pool.end(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/update-influences.js b/scripts/update-influences.js index 86bc667..a241c01 100644 --- a/scripts/update-influences.js +++ b/scripts/update-influences.js @@ -3,208 +3,173 @@ const path = require('path'); const pool = require('../server/db'); const INFLUENCES = require('./art-influences-data'); const { savePaintingImages } = require('./image-fetcher'); +const { discoverInfluencesForWork } = require('./influence-discovery'); +const { + normalizeInfluencedBy, + loadMovements, + resolveWorkRef, + resolveAndInsertSource, +} = require('./influence-resolver'); const FETCH_IMAGES = process.argv.includes('--fetch-images'); +const DISCOVER = process.argv.includes('--discover'); +const DISCOVER_ONLY = process.argv.includes('--discover-only'); +const LIMIT = parseInt(process.argv.find((a) => a.startsWith('--limit='))?.split('=')[1] || '0', 10); const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images')); -const ARTIST_ALIASES = { - 'Camille Corot': 'Jean-Baptiste-Camille Corot', -}; - -function normalizeTitle(s) { - return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); -} - -function normalizeArtist(name) { - return ARTIST_ALIASES[name] || name; -} - -function scoreTitleMatch(dbTitle, hint) { - const normDb = normalizeTitle(dbTitle); - const keywords = normalizeTitle(hint).split(' ').filter((w) => w.length > 2); - if (!keywords.length) return 0; - const hits = keywords.filter((k) => normDb.includes(k)).length; - const required = keywords.length === 1 ? 1 : Math.min(2, keywords.length); - return hits >= required ? hits : 0; -} - -async function loadMovements() { - const { rows } = await pool.query('SELECT id, name FROM art_movements'); - const byName = new Map(rows.map((r) => [r.name.toLowerCase(), r.id])); - return byName; -} - -async function findArtist(name) { - const canonical = normalizeArtist(name); - const { rows } = await pool.query( - `SELECT id, name FROM artists WHERE name = $1 OR name ILIKE $2 LIMIT 1`, - [canonical, canonical] +async function applyEdge(edge, movementsByName, stats) { + const work = await resolveWorkRef( + pool, + edge.work, + movementsByName, + FETCH_IMAGES, + IMAGE_DIR, + savePaintingImages ); - return rows[0] || null; -} - -async function ensureArtist(ref, movementsByName) { - const name = normalizeArtist(ref.artist); - let artist = await findArtist(name); - if (artist) return artist.id; - - const meta = ref.artistMeta; - if (!meta) return null; - - const movementId = meta.movement - ? movementsByName.get(meta.movement.toLowerCase()) || null - : null; - const century = - meta.century || - (meta.birth_year ? Math.floor(meta.birth_year / 100) * 100 : null); - - const insert = await pool.query( - `INSERT INTO artists (name, birth_year, death_year, movement_id, wikipedia_title, century, portrait_path) - VALUES ($1, $2, $3, $4, $5, $6, NULL) - RETURNING id, name`, - [ - name, - meta.birth_year ?? null, - meta.death_year ?? null, - movementId, - meta.wikipedia_title || name, - century, - ] - ); - console.log(`+ artist: ${name}`); - return insert.rows[0].id; -} - -async function findPainting(artistId, titleHint) { - const { rows } = await pool.query( - 'SELECT id, title FROM paintings WHERE artist_id = $1', - [artistId] - ); - let best = null; - let bestScore = 0; - for (const row of rows) { - const score = scoreTitleMatch(row.title, titleHint); - if (score > bestScore) { - bestScore = score; - best = row; - } + if (!work) { + stats.failed += 1; + console.warn(`✗ unresolved work: ${edge.work.artist} / ${edge.work.title}`); + return; } - return best; -} -async function ensurePainting(artistId, ref, artistName) { - const existing = await findPainting(artistId, ref.title); - if (existing) return existing.id; - - const sortRes = await pool.query( - 'SELECT COALESCE(MAX(sort_order), 0) + 1 AS next FROM paintings WHERE artist_id = $1', - [artistId] - ); - const wikiTitle = ref.wikipedia_title || ref.title; - const insert = await pool.query( - `INSERT INTO paintings (artist_id, title, year, wikipedia_title, sort_order) - VALUES ($1, $2, $3, $4, $5) - RETURNING id`, - [artistId, ref.title, ref.year ?? null, wikiTitle, sortRes.rows[0].next] - ); - console.log(`+ painting: ${artistName} — ${ref.title}`); - - if (FETCH_IMAGES) { - try { - const base = `${artistName}_${ref.title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); - const saved = await savePaintingImages(wikiTitle, base, IMAGE_DIR, { - artistName, - paintingTitle: ref.title, - }); - if (saved.imagePath || saved.thumbnailPath) { - await pool.query( - `UPDATE paintings SET image_path = $1, thumbnail_path = $2 WHERE id = $3`, - [saved.imagePath, saved.thumbnailPath, insert.rows[0].id] + const sources = normalizeInfluencedBy(edge.influencedBy); + for (const sourceRef of sources) { + const result = await resolveAndInsertSource( + pool, + work, + sourceRef, + edge, + movementsByName, + FETCH_IMAGES, + IMAGE_DIR, + savePaintingImages + ); + if (!result.ok) { + if (result.reason !== 'self' && result.reason !== 'self artist') { + stats.failed += 1; + console.warn( + `✗ ${edge.work.artist} / ${edge.work.title} <- ${sourceRef.type || 'painting'} ${result.reason}` ); + } else { + stats.skipped += 1; } - } catch (err) { - console.warn(` image fetch failed: ${artistName} — ${ref.title}: ${err.message}`); + continue; + } + if (result.inserted) { + stats.added += 1; + console.log(`→ ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type || 'painting'}] ${result.label}`); + } else { + stats.skipped += 1; } } - return insert.rows[0].id; + if (DISCOVER || DISCOVER_ONLY) { + const discovered = await discoverInfluencesForWork({ + artist: edge.work.artist, + title: edge.work.title, + year: edge.year ?? edge.work.year ?? work.year, + }); + for (const sourceRef of discovered) { + const result = await resolveAndInsertSource( + pool, + work, + sourceRef, + sourceRef, + movementsByName, + false, + IMAGE_DIR, + null + ); + if (result.inserted) { + stats.discovered += 1; + console.log(`~ discovered ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type}] ${result.label}`); + } + } + } } -async function resolvePaintingRef(ref, movementsByName) { - const artistName = normalizeArtist(ref.artist); - const artistId = await ensureArtist(ref, movementsByName); - if (!artistId) return null; - return ensurePainting(artistId, ref, artistName); -} - -async function insertInfluence(workId, sourceId, edge) { - const result = await pool.query( - `INSERT INTO painting_influences - (painting_id, influenced_by_painting_id, notes, source, aspects, quote, source_author, source_url) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (painting_id, influenced_by_painting_id) DO NOTHING - RETURNING id`, - [ - workId, - sourceId, - edge.notes || null, - edge.source || null, - edge.aspects || null, - edge.quote || null, - edge.source_author || null, - edge.source_url || null, - ] +async function discoverCatalog(limit) { + const { rows } = await pool.query( + `SELECT p.id, p.title, p.year, a.name AS artist_name + FROM paintings p + JOIN artists a ON a.id = p.artist_id + ORDER BY a.name, p.sort_order, p.year NULLS LAST` ); - return result.rowCount > 0; + + const targets = limit > 0 ? rows.slice(0, limit) : rows; + const movementsByName = await loadMovements(pool); + let discovered = 0; + + for (const row of targets) { + const refs = await discoverInfluencesForWork({ + artist: row.artist_name, + title: row.title, + year: row.year, + }); + for (const sourceRef of refs) { + const result = await resolveAndInsertSource( + pool, + { id: row.id, year: row.year, artist_id: null }, + sourceRef, + sourceRef, + movementsByName, + false, + IMAGE_DIR, + null + ); + if (result.inserted) { + discovered += 1; + console.log(`~ discovered ${row.artist_name} / ${row.title} ← [${sourceRef.type}] ${result.label}`); + } + } + } + + return discovered; } async function main() { - const movementsByName = await loadMovements(); - let added = 0; - let skipped = 0; - let failed = 0; + const tableCheck = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'painting_influence_sources' + ) AS ok + `); + if (!tableCheck.rows[0]?.ok) { + console.error('Run npm run migrate:influence-sources before update-influences.'); + process.exit(1); + } - for (const edge of INFLUENCES) { - try { - const workId = await resolvePaintingRef(edge.work, movementsByName); - const sourceId = await resolvePaintingRef(edge.influencedBy, movementsByName); + const movementsByName = await loadMovements(pool); + const stats = { added: 0, skipped: 0, failed: 0, discovered: 0 }; - if (!workId || !sourceId) { - failed += 1; - console.warn( - `✗ unresolved: ${edge.work.artist} / ${edge.work.title} <- ${edge.influencedBy?.artist} / ${edge.influencedBy?.title}` - ); - continue; - } - - if (workId === sourceId) { - skipped += 1; - continue; - } - - const inserted = await insertInfluence(workId, sourceId, edge); - if (inserted) { - added += 1; - console.log(`→ ${edge.work.artist} / ${edge.work.title} ← ${edge.influencedBy.artist} / ${edge.influencedBy.title}`); - } else { - skipped += 1; - } - } catch (err) { - failed += 1; - console.warn(`✗ ${edge.work?.artist}: ${err.message}`); + if (!DISCOVER_ONLY) { + const edges = LIMIT > 0 ? INFLUENCES.slice(0, LIMIT) : INFLUENCES; + for (const edge of edges) { + await applyEdge(edge, movementsByName, stats); } } - const total = await pool.query('SELECT COUNT(*)::int AS n FROM painting_influences'); - const connected = await pool.query(` - SELECT COUNT(DISTINCT a.id)::int AS n - FROM artists a - JOIN paintings p ON p.artist_id = a.id - JOIN painting_influences pi ON pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id - `); + if (DISCOVER_ONLY) { + stats.discovered += await discoverCatalog(LIMIT); + } - console.log(`\nDone: ${added} added, ${skipped} skipped (duplicate/self), ${failed} failed`); - console.log(`Total influence edges: ${total.rows[0].n}`); + const [totalSources, totalLegacy, connected] = await Promise.all([ + pool.query('SELECT COUNT(*)::int AS n FROM painting_influence_sources'), + pool.query('SELECT COUNT(*)::int AS n FROM painting_influences'), + pool.query(` + SELECT COUNT(DISTINCT a.id)::int AS n + FROM artists a + JOIN paintings p ON p.artist_id = a.id + LEFT JOIN painting_influence_sources pis ON pis.painting_id = p.id + LEFT JOIN painting_influences pi ON pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id + WHERE pis.id IS NOT NULL OR pi.id IS NOT NULL + `), + ]); + + console.log( + `\nDone: ${stats.added} curated added, ${stats.discovered} discovered, ${stats.skipped} skipped, ${stats.failed} failed` + ); + console.log(`Total influence sources: ${totalSources.rows[0].n} (legacy painting edges: ${totalLegacy.rows[0].n})`); console.log(`Artists connected to influence graph: ${connected.rows[0].n}`); await pool.end(); diff --git a/server/image-service.js b/server/image-service.js index 070c5a7..6224447 100644 --- a/server/image-service.js +++ b/server/image-service.js @@ -1,12 +1,31 @@ const fs = require('fs'); const path = require('path'); const pool = require('./db'); -const { savePaintingImages } = require('../scripts/image-fetcher'); +const { + savePaintingImages, + downloadImageToFile, + generateThumbnailFromFull, + pickExt, +} = require('../scripts/image-fetcher'); const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images'); const FETCH_TIMEOUT_MS = 15000; const inflight = new Map(); +function safePaintingBase(artistName, title) { + return `${artistName}_${title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +function unlinkIfExists(absPath) { + if (absPath && fs.existsSync(absPath)) { + try { + fs.unlinkSync(absPath); + } catch { + // ignore + } + } +} + function withTimeout(promise, ms) { return Promise.race([ promise, @@ -135,4 +154,57 @@ async function ensurePaintingImages(paintingId, size = 'thumb') { return promise; } -module.exports = { ensurePaintingImages, preloadArtistImagesLocal, IMAGE_DIR }; +async function replacePaintingImageFromUrl(paintingId, imageUrl) { + const result = await pool.query( + `SELECT p.id, p.title, p.image_path, p.thumbnail_path, a.name AS artist_name + FROM paintings p + JOIN artists a ON a.id = p.artist_id + WHERE p.id = $1`, + [paintingId] + ); + if (result.rows.length === 0) { + throw new Error('Painting not found'); + } + + const row = result.rows[0]; + const safeBase = safePaintingBase(row.artist_name, row.title); + const paintingsDir = path.join(IMAGE_DIR, 'paintings'); + const thumbsDir = path.join(IMAGE_DIR, 'paintings', 'thumbs'); + if (!fs.existsSync(paintingsDir)) fs.mkdirSync(paintingsDir, { recursive: true }); + if (!fs.existsSync(thumbsDir)) fs.mkdirSync(thumbsDir, { recursive: true }); + + const fullExt = pickExt(imageUrl); + const fullDest = path.join(paintingsDir, safeBase + fullExt); + const thumbDest = path.join(thumbsDir, safeBase + '_thumb.jpg'); + + unlinkIfExists(row.image_path ? path.join(IMAGE_DIR, row.image_path) : null); + unlinkIfExists(row.thumbnail_path ? path.join(IMAGE_DIR, row.thumbnail_path) : null); + unlinkIfExists(fullDest); + unlinkIfExists(thumbDest); + + await downloadImageToFile(imageUrl, fullDest, { force: true }); + + let thumbnailPath = null; + try { + await generateThumbnailFromFull(fullDest, thumbDest); + thumbnailPath = path.join('paintings', 'thumbs', safeBase + '_thumb.jpg').replace(/\\/g, '/'); + } catch { + thumbnailPath = path.join('paintings', safeBase + fullExt).replace(/\\/g, '/'); + } + + const imagePath = path.join('paintings', safeBase + fullExt).replace(/\\/g, '/'); + + await pool.query( + `UPDATE paintings SET image_path = $1, thumbnail_path = $2 WHERE id = $3`, + [imagePath, thumbnailPath, paintingId] + ); + + return { imagePath, thumbnailPath }; +} + +module.exports = { + ensurePaintingImages, + preloadArtistImagesLocal, + replacePaintingImageFromUrl, + IMAGE_DIR, +}; diff --git a/server/index.js b/server/index.js index 1952f93..7fdec67 100644 --- a/server/index.js +++ b/server/index.js @@ -5,7 +5,8 @@ const fs = require('fs'); require('dotenv').config(); const pool = require('./db'); -const { ensurePaintingImages, preloadArtistImagesLocal, IMAGE_DIR } = require('./image-service'); +const { ensurePaintingImages, preloadArtistImagesLocal, replacePaintingImageFromUrl, IMAGE_DIR } = require('./image-service'); +const { searchGoogleImagesFirst, fetchImageBuffer, pickExt } = require('../scripts/image-fetcher'); const app = express(); const PORT = process.env.PORT || 3001; @@ -14,7 +15,50 @@ app.use(cors()); app.use(express.json()); app.use('/images', express.static(IMAGE_DIR)); -// Timeline: eras + movements for a year range +const INFLUENCE_LINKS_EXISTS = ` + EXISTS ( + SELECT 1 FROM painting_influence_sources pis + WHERE pis.painting_id = p.id OR pis.source_painting_id = p.id + ) OR EXISTS ( + SELECT 1 FROM painting_influences pi + WHERE pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id + )`; + +const INFLUENCED_BY_SQL = ` + SELECT + pis.source_type, + pis.period_note, + pis.period_start_year, + pis.period_end_year, + pis.notes, + pis.source, + pis.aspects, + pis.quote, + pis.source_author, + pis.source_url, + pis.confidence, + pis.discovered_via, + p.id, + p.title, + p.year, + p.image_path, + pa.name AS artist_name, + pa.id AS artist_id, + sa.id AS source_artist_id, + sa.name AS source_artist_name, + sa.portrait_path AS artist_portrait, + m.id AS movement_id, + m.name AS movement_name, + m.color AS movement_color + FROM painting_influence_sources pis + LEFT JOIN paintings p ON pis.source_painting_id = p.id + LEFT JOIN artists pa ON p.artist_id = pa.id + LEFT JOIN artists sa ON pis.source_artist_id = sa.id + LEFT JOIN art_movements m ON pis.source_movement_id = m.id + WHERE pis.painting_id = $1 + ORDER BY + CASE pis.source_type WHEN 'painting' THEN 1 WHEN 'artist' THEN 2 ELSE 3 END, + COALESCE(p.year, pis.period_start_year) NULLS LAST`; app.get('/api/timeline', async (req, res) => { try { const { start, end } = req.query; @@ -131,29 +175,51 @@ app.get('/api/artists/:id/navigation', async (req, res) => { ); }; + const predecessorSql = ` + SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, + m.id AS movement_id, m.name AS movement_name, m.color AS movement_color + FROM paintings p_work + JOIN painting_influence_sources pis ON pis.painting_id = p_work.id + LEFT JOIN paintings p_pred ON pis.source_painting_id = p_pred.id + LEFT JOIN artists a ON a.id = COALESCE(p_pred.artist_id, pis.source_artist_id) + LEFT JOIN art_movements m ON a.movement_id = m.id + WHERE p_work.artist_id = $1 AND a.id IS NOT NULL AND a.id <> $1 + UNION + SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, + m.id AS movement_id, m.name AS movement_name, m.color AS movement_color + FROM painting_influences pi + JOIN paintings p_work ON pi.painting_id = p_work.id + JOIN paintings p_pred ON pi.influenced_by_painting_id = p_pred.id + JOIN artists a ON p_pred.artist_id = a.id + LEFT JOIN art_movements m ON a.movement_id = m.id + WHERE p_work.artist_id = $1 AND a.id <> $1`; + + const successorSql = ` + SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, + m.id AS movement_id, m.name AS movement_name, m.color AS movement_color + FROM painting_influence_sources pis + JOIN paintings p_src ON pis.source_painting_id = p_src.id + JOIN paintings p_work ON pis.painting_id = p_work.id + JOIN artists a ON p_work.artist_id = a.id + LEFT JOIN art_movements m ON a.movement_id = m.id + WHERE p_src.artist_id = $1 AND a.id <> $1 + UNION + SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, + m.id AS movement_id, m.name AS movement_name, m.color AS movement_color + FROM painting_influences pi + JOIN paintings p_src ON pi.influenced_by_painting_id = p_src.id + JOIN paintings p_work ON pi.painting_id = p_work.id + JOIN artists a ON p_work.artist_id = a.id + LEFT JOIN art_movements m ON a.movement_id = m.id + WHERE p_src.artist_id = $1 AND a.id <> $1`; + const [predecessors, successors] = await Promise.all([ pool.query( - `SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, - m.id AS movement_id, m.name AS movement_name, m.color AS movement_color - FROM painting_influences pi - JOIN paintings p_work ON pi.painting_id = p_work.id - JOIN paintings p_pred ON pi.influenced_by_painting_id = p_pred.id - JOIN artists a ON p_pred.artist_id = a.id - LEFT JOIN art_movements m ON a.movement_id = m.id - WHERE p_work.artist_id = $1 AND a.id <> $1 - ORDER BY m.name NULLS LAST, a.birth_year NULLS LAST, a.name`, + `SELECT * FROM (${predecessorSql}) u ORDER BY movement_name NULLS LAST, birth_year NULLS LAST, name`, [artistId] ), pool.query( - `SELECT DISTINCT a.id, a.name, a.birth_year, a.death_year, a.portrait_path, - m.id AS movement_id, m.name AS movement_name, m.color AS movement_color - FROM painting_influences pi - JOIN paintings p_src ON pi.influenced_by_painting_id = p_src.id - JOIN paintings p_work ON pi.painting_id = p_work.id - JOIN artists a ON p_work.artist_id = a.id - LEFT JOIN art_movements m ON a.movement_id = m.id - WHERE p_src.artist_id = $1 AND a.id <> $1 - ORDER BY m.name NULLS LAST, a.birth_year NULLS LAST, a.name`, + `SELECT * FROM (${successorSql}) u ORDER BY movement_name NULLS LAST, birth_year NULLS LAST, name`, [artistId] ), ]); @@ -186,10 +252,7 @@ app.get('/api/artists/:id', async (req, res) => { ), pool.query( `SELECT p.*, - EXISTS ( - SELECT 1 FROM painting_influences pi - WHERE pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id - ) AS has_influence_links + (${INFLUENCE_LINKS_EXISTS}) AS has_influence_links FROM paintings p WHERE p.artist_id = $1 ORDER BY p.sort_order, p.year`, @@ -212,6 +275,50 @@ app.get('/api/artists/:id', async (req, res) => { } }); +// Painting image checkup (developer audit table) — must be before /api/paintings/:id +app.get('/api/paintings/checkup', async (_req, res) => { + try { + const { rows } = await pool.query( + `SELECT p.id, p.title, p.year, p.image_path, p.thumbnail_path, a.name AS artist_name + FROM paintings p + JOIN artists a ON a.id = p.artist_id + ORDER BY a.name, p.year NULLS LAST, p.title` + ); + + const paintings = rows.map((row) => { + const galleryFile = row.image_path || row.thumbnail_path || null; + const detailFile = row.image_path || row.thumbnail_path || null; + const detailOnDemand = !detailFile; + + const galleryFileExists = galleryFile + ? fs.existsSync(path.join(IMAGE_DIR, galleryFile)) + : false; + const detailFileExists = detailFile + ? fs.existsSync(path.join(IMAGE_DIR, detailFile)) + : false; + + return { + id: row.id, + title: row.title, + artist_name: row.artist_name, + year: row.year, + gallery_file: galleryFile, + gallery_preview: row.thumbnail_path || galleryFile, + detail_file: detailOnDemand ? `/api/paintings/${row.id}/image?size=full` : detailFile, + detail_preview: row.thumbnail_path || (detailOnDemand ? null : detailFile), + detail_on_demand: detailOnDemand, + gallery_file_exists: galleryFileExists, + detail_file_exists: detailOnDemand ? null : detailFileExists, + }; + }); + + res.json({ paintings, total: paintings.length }); + } catch (err) { + console.error('Checkup error:', err.message); + res.status(500).json({ error: 'Failed to load checkup data' }); + } +}); + // Painting detail with influences app.get('/api/paintings/:id', async (req, res) => { try { @@ -219,10 +326,7 @@ app.get('/api/paintings/:id', async (req, res) => { const painting = await pool.query( `SELECT p.*, a.name as artist_name, a.id as artist_id, a.portrait_path as artist_portrait, - EXISTS ( - SELECT 1 FROM painting_influences pi - WHERE pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id - ) AS has_influence_links + (${INFLUENCE_LINKS_EXISTS}) AS has_influence_links FROM paintings p JOIN artists a ON p.artist_id = a.id WHERE p.id = $1`, @@ -234,22 +338,26 @@ app.get('/api/paintings/:id', async (req, res) => { } const [influencedBy, influenced] = await Promise.all([ + pool.query(INFLUENCED_BY_SQL, [id]), pool.query( - `SELECT pi.notes, pi.source, pi.aspects, pi.quote, pi.source_author, pi.source_url, - p.id, p.title, p.year, p.image_path, a.name as artist_name, a.id as artist_id - FROM painting_influences pi - JOIN paintings p ON pi.influenced_by_painting_id = p.id - JOIN artists a ON p.artist_id = a.id - WHERE pi.painting_id = $1`, - [id] - ), - pool.query( - `SELECT pi.notes, pi.source, pi.aspects, pi.quote, pi.source_author, pi.source_url, - p.id, p.title, p.year, p.image_path, a.name as artist_name, a.id as artist_id - FROM painting_influences pi - JOIN paintings p ON pi.painting_id = p.id - JOIN artists a ON p.artist_id = a.id - WHERE pi.influenced_by_painting_id = $1`, + `SELECT * FROM ( + SELECT pi.notes, pi.source, pi.aspects, pi.quote, pi.source_author, pi.source_url, + 'painting' AS source_type, + p.id, p.title, p.year, p.image_path, a.name as artist_name, a.id as artist_id + FROM painting_influences pi + JOIN paintings p ON pi.painting_id = p.id + JOIN artists a ON p.artist_id = a.id + WHERE pi.influenced_by_painting_id = $1 + UNION ALL + SELECT pis.notes, pis.source, pis.aspects, pis.quote, pis.source_author, pis.source_url, + 'painting' AS source_type, + p.id, p.title, p.year, p.image_path, a.name as artist_name, a.id as artist_id + FROM painting_influence_sources pis + JOIN paintings p ON pis.painting_id = p.id + JOIN artists a ON p.artist_id = a.id + WHERE pis.source_painting_id = $1 AND pis.source_type = 'painting' + ) influenced_works + ORDER BY year NULLS LAST`, [id] ), ]); @@ -277,6 +385,68 @@ app.post('/api/artists/:id/preload-images', async (req, res) => { } }); +// Developer debug: Google Images first result for image audit +app.get('/api/paintings/:id/debug-image-search', async (req, res) => { + try { + const paintingId = parseInt(req.params.id, 10); + const result = await pool.query( + `SELECT p.title, a.name AS artist_name + FROM paintings p + JOIN artists a ON a.id = p.artist_id + WHERE p.id = $1`, + [paintingId] + ); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Painting not found' }); + } + + const { title, artist_name: artistName } = result.rows[0]; + const search = await searchGoogleImagesFirst(artistName, title); + res.json(search); + } catch (err) { + console.error('Debug image search error:', err.message); + res.status(500).json({ error: 'Image search failed' }); + } +}); + +// Developer debug: replace painting image with a search result URL +app.post('/api/paintings/:id/fix-image', async (req, res) => { + try { + const paintingId = parseInt(req.params.id, 10); + const imageUrl = req.body?.imageUrl; + if (!imageUrl || typeof imageUrl !== 'string' || !/^https?:\/\//i.test(imageUrl)) { + return res.status(400).json({ error: 'Valid imageUrl required' }); + } + + const updated = await replacePaintingImageFromUrl(paintingId, imageUrl); + res.json(updated); + } catch (err) { + console.error('Fix image error:', err.message); + res.status(500).json({ error: err.message || 'Fix image failed' }); + } +}); + +// Proxy remote image for debug preview (avoids hotlink / CORS blocks) +app.get('/api/debug/image-proxy', async (req, res) => { + try { + const imageUrl = req.query.url; + if (!imageUrl || typeof imageUrl !== 'string' || !/^https?:\/\//i.test(imageUrl)) { + return res.status(400).json({ error: 'Valid url query required' }); + } + + const buffer = await fetchImageBuffer(imageUrl); + const ext = pickExt(imageUrl).toLowerCase(); + const type = + ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'; + res.setHeader('Content-Type', type); + res.setHeader('Cache-Control', 'no-store'); + res.send(buffer); + } catch (err) { + console.error('Image proxy error:', err.message); + res.status(502).json({ error: 'Could not load preview image' }); + } +}); + // On-demand painting image (resolves, caches, serves thumb or full) app.get('/api/paintings/:id/image', async (req, res) => { try {