diff --git a/.env.example b/.env.example index 09199cb..6d4322f 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ DB_HOST=192.168.10.122 DB_PORT=5432 DB_USER=gallery -DB_PASSWORD= +DB_PASSWORD=gallery DB_NAME=Gallery # Production: LAN http://192.168.10.70:3520 and reverse-proxy at gallery.mysuperlab.netcraze.pro diff --git a/Documentation/setup.md b/Documentation/setup.md index 3a99222..56863d8 100644 --- a/Documentation/setup.md +++ b/Documentation/setup.md @@ -139,7 +139,11 @@ Or use the systemd unit in [`deploy/gallery.service`](../deploy/gallery.service) Point `gallery.mysuperlab.netcraze.pro` at the host running the app. Example nginx config: [`deploy/nginx-gallery.conf`](../deploy/nginx-gallery.conf). -The proxy forwards to `127.0.0.1:3520`. Keep `TRUST_PROXY=true` in `.env` so Express sees the correct client IP and scheme. +**Development (default in repo):** nginx forwards to **Vite on `127.0.0.1:5173`**. Run both `npm run dev:server` (API on `3520`) and `npm run dev:client` (`5173`). Vite proxies `/api` and `/images` to the API. + +**Production (built SPA):** change nginx `proxy_pass` to `http://127.0.0.1:3520` after `npm run build` and `npm run start` — Node serves `client/dist` and the API on one port. + +Keep `TRUST_PROXY=true` in `.env` so Express sees the correct client IP and scheme. ### 4. Firewall @@ -203,7 +207,7 @@ These are checked in and maintained: - `migrate-painting-annotations.js`, `update-painting-annotations.js`, `painting-annotations-data.js` — art-history notes on painting detail - `regenerate-thumbnails.js`, `audit-painting-images.js` -These are referenced in `package.json` but may need to be restored from git history if missing locally: `seed-wikipedia.js`, `fetch-artist-images.js`, `sync-image-paths.js`. +These are referenced in `package.json` but may need to be restored from git history if missing locally: `fetch-artist-images.js`, `sync-image-paths.js`. See [data-and-images.md](data-and-images.md) for pipeline details and override maps. diff --git a/client/vite.config.ts b/client/vite.config.ts index 1cdb4c6..187b881 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -10,6 +10,8 @@ export default defineConfig(({ mode }) => { return { plugins: [react()], server: { + host: true, + allowedHosts: ['gallery.mysuperlab.netcraze.pro', 'localhost'], proxy: { '/api': apiTarget, '/images': apiTarget, diff --git a/data/images/paintings/Albrecht_D_rer_Self-Portrait.jpg b/data/images/paintings/Albrecht_D_rer_Self-Portrait.jpg new file mode 100644 index 0000000..3de10ac Binary files /dev/null and b/data/images/paintings/Albrecht_D_rer_Self-Portrait.jpg differ diff --git a/data/images/paintings/Andrei_Rublev_Trinity.jpg b/data/images/paintings/Andrei_Rublev_Trinity.jpg new file mode 100644 index 0000000..c2bbc50 Binary files /dev/null and b/data/images/paintings/Andrei_Rublev_Trinity.jpg differ diff --git a/data/images/paintings/Caravaggio_Calling_of_Saint_Matthew.jpg b/data/images/paintings/Caravaggio_Calling_of_Saint_Matthew.jpg new file mode 100644 index 0000000..97f6f11 Binary files /dev/null and b/data/images/paintings/Caravaggio_Calling_of_Saint_Matthew.jpg differ diff --git a/data/images/paintings/Domenico_Ghirlandaio_Birth_of_the_Virgin.jpg b/data/images/paintings/Domenico_Ghirlandaio_Birth_of_the_Virgin.jpg new file mode 100644 index 0000000..e774f24 Binary files /dev/null and b/data/images/paintings/Domenico_Ghirlandaio_Birth_of_the_Virgin.jpg differ diff --git a/data/images/paintings/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte.jpg b/data/images/paintings/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte.jpg new file mode 100644 index 0000000..38cc460 Binary files /dev/null and b/data/images/paintings/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte.jpg differ diff --git a/data/images/paintings/Hieronymus_Bosch_Garden_of_Earthly_Delights.jpg b/data/images/paintings/Hieronymus_Bosch_Garden_of_Earthly_Delights.jpg new file mode 100644 index 0000000..43231f8 Binary files /dev/null and b/data/images/paintings/Hieronymus_Bosch_Garden_of_Earthly_Delights.jpg differ diff --git a/data/images/paintings/Parrhasius_Theseus.jpg b/data/images/paintings/Parrhasius_Theseus.jpg new file mode 100644 index 0000000..fba1d8b Binary files /dev/null and b/data/images/paintings/Parrhasius_Theseus.jpg differ diff --git a/data/images/paintings/Peter_Paul_Rubens_Descent_from_the_Cross.jpg b/data/images/paintings/Peter_Paul_Rubens_Descent_from_the_Cross.jpg new file mode 100644 index 0000000..452ea24 Binary files /dev/null and b/data/images/paintings/Peter_Paul_Rubens_Descent_from_the_Cross.jpg differ diff --git a/data/images/paintings/Raphael_Sistine_Madonna.jpg b/data/images/paintings/Raphael_Sistine_Madonna.jpg new file mode 100644 index 0000000..33bd01e Binary files /dev/null and b/data/images/paintings/Raphael_Sistine_Madonna.jpg differ diff --git a/data/images/paintings/Titian_Venus_of_Urbino.jpg b/data/images/paintings/Titian_Venus_of_Urbino.jpg new file mode 100644 index 0000000..c1c1924 Binary files /dev/null and b/data/images/paintings/Titian_Venus_of_Urbino.jpg differ diff --git a/data/images/paintings/thumbs/Albrecht_D_rer_Self-Portrait_thumb.jpg b/data/images/paintings/thumbs/Albrecht_D_rer_Self-Portrait_thumb.jpg new file mode 100644 index 0000000..dbc5e5e Binary files /dev/null and b/data/images/paintings/thumbs/Albrecht_D_rer_Self-Portrait_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg b/data/images/paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg new file mode 100644 index 0000000..c468f19 Binary files /dev/null and b/data/images/paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Caravaggio_Calling_of_Saint_Matthew_thumb.jpg b/data/images/paintings/thumbs/Caravaggio_Calling_of_Saint_Matthew_thumb.jpg new file mode 100644 index 0000000..43f9074 Binary files /dev/null and b/data/images/paintings/thumbs/Caravaggio_Calling_of_Saint_Matthew_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Domenico_Ghirlandaio_Birth_of_the_Virgin_thumb.jpg b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Birth_of_the_Virgin_thumb.jpg new file mode 100644 index 0000000..7d78cb0 Binary files /dev/null and b/data/images/paintings/thumbs/Domenico_Ghirlandaio_Birth_of_the_Virgin_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte_thumb.jpg b/data/images/paintings/thumbs/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte_thumb.jpg new file mode 100644 index 0000000..621fd98 Binary files /dev/null and b/data/images/paintings/thumbs/Georges_Seurat_Sunday_Afternoon_on_the_Island_of_La_Grande_Jatte_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Hieronymus_Bosch_Garden_of_Earthly_Delights_thumb.jpg b/data/images/paintings/thumbs/Hieronymus_Bosch_Garden_of_Earthly_Delights_thumb.jpg new file mode 100644 index 0000000..b7053f2 Binary files /dev/null and b/data/images/paintings/thumbs/Hieronymus_Bosch_Garden_of_Earthly_Delights_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Jacques-Louis_David_Oath_of_the_Horatii_thumb.jpg b/data/images/paintings/thumbs/Jacques-Louis_David_Oath_of_the_Horatii_thumb.jpg new file mode 100644 index 0000000..a280ab9 Binary files /dev/null and b/data/images/paintings/thumbs/Jacques-Louis_David_Oath_of_the_Horatii_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Parrhasius_Theseus_thumb.jpg b/data/images/paintings/thumbs/Parrhasius_Theseus_thumb.jpg new file mode 100644 index 0000000..4bd84b8 Binary files /dev/null and b/data/images/paintings/thumbs/Parrhasius_Theseus_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Peter_Paul_Rubens_Descent_from_the_Cross_thumb.jpg b/data/images/paintings/thumbs/Peter_Paul_Rubens_Descent_from_the_Cross_thumb.jpg new file mode 100644 index 0000000..43c2941 Binary files /dev/null and b/data/images/paintings/thumbs/Peter_Paul_Rubens_Descent_from_the_Cross_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Raphael_Sistine_Madonna_thumb.jpg b/data/images/paintings/thumbs/Raphael_Sistine_Madonna_thumb.jpg new file mode 100644 index 0000000..c68bf30 Binary files /dev/null and b/data/images/paintings/thumbs/Raphael_Sistine_Madonna_thumb.jpg differ diff --git a/data/images/paintings/thumbs/Titian_Venus_of_Urbino_thumb.jpg b/data/images/paintings/thumbs/Titian_Venus_of_Urbino_thumb.jpg new file mode 100644 index 0000000..328790e Binary files /dev/null and b/data/images/paintings/thumbs/Titian_Venus_of_Urbino_thumb.jpg differ diff --git a/deploy/nginx-gallery.conf b/deploy/nginx-gallery.conf index 47000da..984ab74 100644 --- a/deploy/nginx-gallery.conf +++ b/deploy/nginx-gallery.conf @@ -1,4 +1,9 @@ -# Reverse proxy for gallery.mysuperlab.netcraze.pro → Node on port 3520. +# Reverse proxy for gallery.mysuperlab.netcraze.pro → Vite dev client on :5173. +# Vite proxies /api and /images to the Node API on :3520 (see client/vite.config.ts). +# +# Dev stack: npm run dev:server && npm run dev:client +# Production (built SPA on Node only): point location / at http://127.0.0.1:3520 instead. +# # Install: copy to /etc/nginx/sites-available/gallery, symlink sites-enabled, nginx -t && systemctl reload nginx server { @@ -8,8 +13,10 @@ server { client_max_body_size 20M; location / { - proxy_pass http://127.0.0.1:3520; + proxy_pass http://127.0.0.1:5173; proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/scripts/seed-catalog-data.js b/scripts/seed-catalog-data.js new file mode 100644 index 0000000..08f2330 --- /dev/null +++ b/scripts/seed-catalog-data.js @@ -0,0 +1,282 @@ +/** + * Curated eras, movements, and artist metadata for npm run seed. + * Flagship paintings are resolved from art-influences-data.js and famous-paintings-data.js. + */ +const FAMOUS = require('./famous-paintings-data'); +const INFLUENCES = require('./art-influences-data'); + +const ERAS = [ + { name: 'Ancient', start_year: -3000, end_year: 500, start_definite: false, end_definite: false, sort_order: 1, + description: 'Classical antiquity — Greek and Roman art foundations.' }, + { name: 'Medieval', start_year: 500, end_year: 1400, start_definite: false, end_definite: false, sort_order: 2, + description: 'Byzantine icons, Gothic cathedrals, and late medieval panel painting.' }, + { name: 'Renaissance', start_year: 1400, end_year: 1600, start_definite: false, end_definite: false, sort_order: 3, + description: 'Humanism, perspective, and the rebirth of classical form in Italy and the North.' }, + { name: 'Baroque & Rococo', start_year: 1600, end_year: 1750, start_definite: false, end_definite: false, sort_order: 4, + description: 'Drama, movement, and decorative splendour across Europe.' }, + { name: 'Neoclassical & Romantic', start_year: 1750, end_year: 1850, start_definite: false, end_definite: false, sort_order: 5, + description: 'Revival of antiquity and the Romantic sublime.' }, + { name: 'Modern', start_year: 1850, end_year: 1950, start_definite: true, end_definite: true, sort_order: 6, + description: 'Impressionism through mid-century avant-garde.' }, + { name: 'Contemporary', start_year: 1950, end_year: 2100, start_definite: true, end_definite: false, sort_order: 7, + description: 'Post-war and contemporary art.' }, +]; + +const MOVEMENTS = [ + { name: 'Ancient Greek & Roman', start_year: -800, end_year: 500, era: 'Ancient', color: '#C4A574', description: 'Classical Greek and Roman painting and sculpture.' }, + { name: 'Byzantine', start_year: 330, end_year: 1453, era: 'Medieval', color: '#D4AF37', description: 'Gold-ground icons and mosaic traditions of the Eastern Empire.' }, + { name: 'Gothic', start_year: 1150, end_year: 1500, era: 'Medieval', color: '#6B7B8C', description: 'International Gothic and proto-Renaissance Italy.' }, + { name: 'Early Renaissance', start_year: 1400, end_year: 1490, era: 'Renaissance', color: '#C9A227', description: 'Florentine quattrocento — Masaccio, Botticelli, Ghirlandaio.' }, + { name: 'Northern Renaissance', start_year: 1420, end_year: 1580, era: 'Renaissance', color: '#8B7355', description: 'Van Eyck, Dürer, and Flemish oil technique.' }, + { name: 'High Renaissance', start_year: 1490, end_year: 1527, era: 'Renaissance', color: '#B8860B', description: 'Leonardo, Michelangelo, Raphael, and Venetian color.' }, + { name: 'Mannerism', start_year: 1520, end_year: 1600, era: 'Renaissance', color: '#9B6B9E', description: 'Elongated forms and artificial elegance after the High Renaissance.' }, + { name: 'Baroque', start_year: 1600, end_year: 1750, era: 'Baroque & Rococo', color: '#8B2500', description: 'Caravaggio, Rubens, Rembrandt — drama and grandeur.' }, + { name: 'Rococo', start_year: 1700, end_year: 1770, era: 'Baroque & Rococo', color: '#E8B4CB', description: 'Watteau, Fragonard — lightness, pastel, and aristocratic play.' }, + { name: 'Neoclassicism', start_year: 1760, end_year: 1850, era: 'Neoclassical & Romantic', color: '#D4C4A8', description: 'David and Ingres revive classical severity.' }, + { name: 'Romanticism', start_year: 1800, end_year: 1850, era: 'Neoclassical & Romantic', color: '#4A6741', description: 'Delacroix, Turner, Friedrich — emotion, nature, and the sublime.' }, + { name: 'Realism', start_year: 1840, end_year: 1880, era: 'Modern', color: '#6B5B4F', description: 'Courbet and Millet paint modern life without idealisation.' }, + { name: 'Impressionism', start_year: 1860, end_year: 1890, era: 'Modern', color: '#87CEEB', description: 'Monet, Renoir, Degas — light, atmosphere, and plein-air colour.' }, + { name: 'Post-Impressionism', start_year: 1880, end_year: 1905, era: 'Modern', color: '#E8A54B', description: 'Cézanne, Van Gogh, Gauguin — personal vision beyond Impressionism.' }, + { name: 'Symbolism', start_year: 1880, end_year: 1910, era: 'Modern', color: '#5C4A72', description: 'Redon, Klimt, Böcklin — dream, myth, and interior feeling.' }, + { name: 'Art Nouveau', start_year: 1890, end_year: 1910, era: 'Modern', color: '#6A9868', description: 'Mucha, Beardsley — organic line and decorative modernity.' }, + { name: 'Fauvism', start_year: 1904, end_year: 1908, era: 'Modern', color: '#E85D04', description: 'Matisse and Derain — wild colour and expressive brushwork.' }, + { name: 'Expressionism', start_year: 1905, end_year: 1925, era: 'Modern', color: '#C41E3A', description: 'Munch, Kirchner, Kandinsky — subjective emotion and distortion.' }, + { name: 'Cubism', start_year: 1907, end_year: 1920, era: 'Modern', color: '#8B8B7A', description: 'Picasso and Braque fracture form into shifting planes.' }, + { name: 'Futurism', start_year: 1909, end_year: 1940, era: 'Modern', color: '#CC5500', description: 'Boccioni and Balla celebrate speed, machine, and dynamism.' }, + { name: 'Suprematism', start_year: 1913, end_year: 1920, era: 'Modern', color: '#1A1A1A', description: 'Malevich and pure geometric abstraction.' }, + { name: 'Constructivism', start_year: 1913, end_year: 1930, era: 'Modern', color: '#B22222', description: 'Tatlin, Rodchenko — art in service of social construction.' }, + { name: 'Dada', start_year: 1916, end_year: 1924, era: 'Modern', color: '#708090', description: 'Duchamp, Höch — anti-art and absurd provocation.' }, + { name: 'Surrealism', start_year: 1924, end_year: 1960, era: 'Modern', color: '#4B0082', description: 'Dalí, Magritte, Miró — dreams, the unconscious, and impossible worlds.' }, + { name: 'Abstract Expressionism', start_year: 1940, end_year: 1960, era: 'Contemporary', color: '#2F4F4F', description: 'Pollock, Rothko, de Kooning — gestural and colour-field abstraction.' }, + { name: 'Pop Art', start_year: 1955, end_year: 1970, era: 'Contemporary', color: '#FF6B6B', description: 'Warhol, Lichtenstein — mass media and consumer culture.' }, +]; + +/** Wikipedia article title overrides (aligned with fetch-artist-bios.js). */ +const WIKI_OVERRIDES = { + Zeuxis: 'Zeuxis (painter)', + 'Ivan Klyun': 'Ivan Kliun', + 'Jean-Antoine Watteau': 'Antoine Watteau', +}; + +/** Birth and death years; movement when not inferable from influence metadata. */ +const ARTIST_META = { + Apelles: { birth: -400, death: -320, movement: 'Ancient Greek & Roman' }, + Parrhasius: { birth: -450, death: -380, movement: 'Ancient Greek & Roman' }, + Zeuxis: { birth: -500, death: -400, movement: 'Ancient Greek & Roman' }, + Giotto: { birth: 1267, death: 1337, movement: 'Gothic' }, + Cimabue: { birth: 1240, death: 1302, movement: 'Byzantine' }, + Duccio: { birth: 1255, death: 1319, movement: 'Gothic' }, + 'Simone Martini': { birth: 1284, death: 1344, movement: 'Gothic' }, + 'Andrei Rublev': { birth: 1360, death: 1430, movement: 'Byzantine' }, + 'Theophanes the Greek': { birth: 1340, death: 1410, movement: 'Byzantine' }, + Masaccio: { birth: 1401, death: 1428, movement: 'Early Renaissance' }, + 'Fra Angelico': { birth: 1395, death: 1455, movement: 'Early Renaissance' }, + 'Jan van Eyck': { birth: 1390, death: 1441, movement: 'Northern Renaissance' }, + 'Sandro Botticelli': { birth: 1445, death: 1510, movement: 'Early Renaissance' }, + 'Domenico Ghirlandaio': { birth: 1448, death: 1494, movement: 'Early Renaissance' }, + 'Andrea del Castagno': { birth: 1421, death: 1457, movement: 'Early Renaissance' }, + 'Leonardo da Vinci': { birth: 1452, death: 1519, movement: 'High Renaissance' }, + Michelangelo: { birth: 1475, death: 1564, movement: 'High Renaissance' }, + Raphael: { birth: 1483, death: 1520, movement: 'High Renaissance' }, + Giorgione: { birth: 1477, death: 1510, movement: 'High Renaissance' }, + Titian: { birth: 1488, death: 1576, movement: 'High Renaissance' }, + 'Albrecht Dürer': { birth: 1471, death: 1528, movement: 'Northern Renaissance' }, + Pontormo: { birth: 1494, death: 1557, movement: 'Mannerism' }, + Parmigianino: { birth: 1503, death: 1540, movement: 'Mannerism' }, + Bronzino: { birth: 1503, death: 1572, movement: 'Mannerism' }, + 'Hieronymus Bosch': { birth: 1450, death: 1516, movement: 'Northern Renaissance' }, + Caravaggio: { birth: 1571, death: 1610, movement: 'Baroque' }, + 'Artemisia Gentileschi': { birth: 1593, death: 1656, movement: 'Baroque' }, + 'Peter Paul Rubens': { birth: 1577, death: 1640, movement: 'Baroque' }, + Rembrandt: { birth: 1606, death: 1669, movement: 'Baroque' }, + 'Diego Velázquez': { birth: 1599, death: 1660, movement: 'Baroque' }, + 'El Greco': { birth: 1541, death: 1614, movement: 'Mannerism' }, + 'Nicolas Poussin': { birth: 1594, death: 1665, movement: 'Baroque' }, + 'Claude Lorrain': { birth: 1600, death: 1682, movement: 'Baroque' }, + 'François Boucher': { birth: 1703, death: 1770, movement: 'Rococo' }, + 'Jean-Antoine Watteau': { birth: 1684, death: 1721, movement: 'Rococo' }, + 'Jean-Honoré Fragonard': { birth: 1732, death: 1806, movement: 'Rococo' }, + 'Jacques-Louis David': { birth: 1748, death: 1825, movement: 'Neoclassicism' }, + 'Jean-Auguste-Dominique Ingres': { birth: 1780, death: 1867, movement: 'Neoclassicism' }, + 'Eugène Delacroix': { birth: 1798, death: 1863, movement: 'Romanticism' }, + 'Théodore Géricault': { birth: 1791, death: 1824, movement: 'Romanticism' }, + 'Caspar David Friedrich': { birth: 1774, death: 1840, movement: 'Romanticism' }, + 'J. M. W. Turner': { birth: 1775, death: 1851, movement: 'Romanticism' }, + 'John Constable': { birth: 1776, death: 1837, movement: 'Romanticism' }, + 'Jean-François Millet': { birth: 1814, death: 1875, movement: 'Realism' }, + 'Gustave Courbet': { birth: 1819, death: 1877, movement: 'Realism' }, + 'Édouard Manet': { birth: 1832, death: 1883, movement: 'Realism' }, + 'Claude Monet': { birth: 1840, death: 1926, movement: 'Impressionism' }, + 'Edgar Degas': { birth: 1834, death: 1917, movement: 'Impressionism' }, + 'Pierre-Auguste Renoir': { birth: 1841, death: 1919, movement: 'Impressionism' }, + 'Camille Pissarro': { birth: 1830, death: 1903, movement: 'Impressionism' }, + 'Henri de Toulouse-Lautrec': { birth: 1864, death: 1901, movement: 'Post-Impressionism' }, + 'Paul Cézanne': { birth: 1839, death: 1906, movement: 'Post-Impressionism' }, + 'Vincent van Gogh': { birth: 1853, death: 1890, movement: 'Post-Impressionism' }, + 'Paul Gauguin': { birth: 1848, death: 1903, movement: 'Post-Impressionism' }, + 'Georges Seurat': { birth: 1859, death: 1891, movement: 'Post-Impressionism' }, + 'Edvard Munch': { birth: 1863, death: 1944, movement: 'Expressionism' }, + 'Henri Matisse': { birth: 1869, death: 1954, movement: 'Fauvism' }, + 'André Derain': { birth: 1880, death: 1954, movement: 'Fauvism' }, + 'Maurice de Vlaminck': { birth: 1876, death: 1958, movement: 'Fauvism' }, + 'Georges Braque': { birth: 1882, death: 1963, movement: 'Cubism' }, + 'Pablo Picasso': { birth: 1881, death: 1973, movement: 'Cubism' }, + 'Juan Gris': { birth: 1887, death: 1927, movement: 'Cubism' }, + 'Henri Privat-Livemont': { birth: 1861, death: 1936, movement: 'Art Nouveau' }, + 'Alphonse Mucha': { birth: 1860, death: 1939, movement: 'Art Nouveau' }, + 'Gustav Klimt': { birth: 1862, death: 1918, movement: 'Symbolism' }, + 'Odilon Redon': { birth: 1840, death: 1916, movement: 'Symbolism' }, + 'Arnold Böcklin': { birth: 1827, death: 1901, movement: 'Symbolism' }, + 'Pierre Puvis de Chavannes': { birth: 1824, death: 1898, movement: 'Symbolism' }, + 'Aubrey Beardsley': { birth: 1872, death: 1898, movement: 'Art Nouveau' }, + 'Angelica Kauffman': { birth: 1741, death: 1807, movement: 'Neoclassicism' }, + 'Francisco Goya': { birth: 1746, death: 1828, movement: 'Romanticism' }, + 'Honoré Daumier': { birth: 1808, death: 1879, movement: 'Realism' }, + 'Jean-Baptiste-Camille Corot': { birth: 1796, death: 1875, movement: 'Realism' }, + 'Kazimir Malevich': { birth: 1879, death: 1935, movement: 'Suprematism' }, + 'Wassily Kandinsky': { birth: 1866, death: 1944, movement: 'Expressionism' }, + 'Ernst Ludwig Kirchner': { birth: 1880, death: 1938, movement: 'Expressionism' }, + 'Umberto Boccioni': { birth: 1882, death: 1916, movement: 'Futurism' }, + 'Giacomo Balla': { birth: 1871, death: 1958, movement: 'Futurism' }, + 'Gino Severini': { birth: 1883, death: 1966, movement: 'Futurism' }, + 'Vladimir Tatlin': { birth: 1885, death: 1953, movement: 'Constructivism' }, + 'Alexander Rodchenko': { birth: 1891, death: 1956, movement: 'Constructivism' }, + 'Lyubov Popova': { birth: 1889, death: 1924, movement: 'Constructivism' }, + 'Olga Rozanova': { birth: 1886, death: 1918, movement: 'Suprematism' }, + 'Ivan Klyun': { birth: 1873, death: 1943, movement: 'Suprematism' }, + 'Marcel Duchamp': { birth: 1887, death: 1968, movement: 'Dada' }, + 'Francis Picabia': { birth: 1879, death: 1953, movement: 'Dada' }, + 'Hannah Höch': { birth: 1889, death: 1978, movement: 'Dada' }, + 'Max Ernst': { birth: 1891, death: 1976, movement: 'Surrealism' }, + 'Giorgio de Chirico': { birth: 1888, death: 1978, movement: 'Surrealism' }, + 'Salvador Dalí': { birth: 1904, death: 1989, movement: 'Surrealism' }, + 'René Magritte': { birth: 1898, death: 1967, movement: 'Surrealism' }, + 'Joan Miró': { birth: 1893, death: 1983, movement: 'Surrealism' }, + 'Jackson Pollock': { birth: 1912, death: 1956, movement: 'Abstract Expressionism' }, + 'Mark Rothko': { birth: 1903, death: 1970, movement: 'Abstract Expressionism' }, + 'Willem de Kooning': { birth: 1904, death: 1997, movement: 'Abstract Expressionism' }, + 'Andy Warhol': { birth: 1928, death: 1987, movement: 'Pop Art' }, + 'Roy Lichtenstein': { birth: 1923, death: 1997, movement: 'Pop Art' }, + 'Richard Hamilton': { birth: 1922, death: 2011, movement: 'Pop Art' }, +}; + +/** Flagship painting when not present in influence / famous data files. */ +const FLAGSHIP_OVERRIDES = { + Giotto: { title: 'Lamentation', year: 1305, wikipedia_title: 'Kiss of Judas (Giotto)' }, + 'Jan van Eyck': { title: 'The Arnolfini Portrait', year: 1434, wikipedia_title: 'Arnolfini Portrait' }, + Giorgione: { title: 'The Tempest', year: 1508, wikipedia_title: 'The Tempest (Giorgione)' }, + 'Andrea del Castagno': { title: 'Last Supper', year: 1447, wikipedia_title: 'Last Supper (Castagno)' }, + 'Claude Lorrain': { title: 'Embarkation of the Queen of Sheba', year: 1648, wikipedia_title: 'Embarkation of the Queen of Sheba' }, + 'Nicolas Poussin': { title: 'The Rape of the Sabine Women', year: 1637, wikipedia_title: 'The Rape of the Sabine Women (Nicolas Poussin)' }, + 'Jean-Baptiste-Camille Corot': { title: 'Forest of Fontainebleau', year: 1846, wikipedia_title: 'Forest of Fontainebleau (Corot)' }, + 'John Constable': { title: 'The Hay Wain', year: 1821, wikipedia_title: 'The Hay Wain' }, + 'Théodore Géricault': { title: 'The Raft of the Medusa', year: 1819, wikipedia_title: 'The Raft of the Medusa' }, + 'Arnold Böcklin': { title: 'Isle of the Dead', year: 1880, wikipedia_title: 'Isle of the Dead (painting)' }, + 'Andrei Rublev': { title: 'Trinity', year: 1411, wikipedia_title: 'Trinity (Andrei Rublev)' }, + Cimabue: { title: 'Santa Trinita Maestà', year: 1290, wikipedia_title: 'Santa Trinita Maestà' }, + Duccio: { title: 'Maestà', year: 1311, wikipedia_title: 'Maestà (Duccio)' }, + 'Fra Angelico': { title: 'The Annunciation', year: 1443, wikipedia_title: 'Annunciation (Fra Angelico, San Marco)' }, + 'Simone Martini': { title: 'Annunciation', year: 1333, wikipedia_title: 'Annunciation with St. Margaret and St. Ansanus' }, + 'Domenico Ghirlandaio': { title: 'Birth of the Virgin', year: 1486, wikipedia_title: 'Birth of the Virgin (Ghirlandaio)' }, +}; + +function centuryFromBirth(birth) { + if (birth == null) return 1800; + return birth >= 0 ? Math.floor(birth / 100) * 100 : Math.floor(birth / 100) * 100; +} + +function walkInfluence(node, visit) { + if (!node) return; + if (node.artist) visit(node); + if (Array.isArray(node.influencedBy)) node.influencedBy.forEach((n) => walkInfluence(n, visit)); + else if (node.influencedBy) walkInfluence(node.influencedBy, visit); +} + +function collectFlagships() { + const map = new Map(); + for (const edge of INFLUENCES) { + if (edge.work?.artist && edge.work?.title && !map.has(edge.work.artist)) { + map.set(edge.work.artist, { + title: edge.work.title, + year: edge.work.year ?? null, + wikipedia_title: edge.work.wikipedia_title || edge.work.title, + }); + } + walkInfluence(edge.influencedBy, (node) => { + if (node.artist && node.title && !map.has(node.artist)) { + map.set(node.artist, { + title: node.title, + year: node.year ?? null, + wikipedia_title: node.wikipedia_title || node.title, + }); + } + }); + } + for (const entry of FAMOUS) { + if (!map.has(entry.artist)) { + map.set(entry.artist, { + title: entry.title, + year: entry.year ?? null, + wikipedia_title: entry.wikipedia_title || entry.title, + }); + } + } + for (const [artist, painting] of Object.entries(FLAGSHIP_OVERRIDES)) { + if (!map.has(artist)) map.set(artist, painting); + } + return map; +} + +function collectArtistNames() { + const names = new Set(Object.keys(ARTIST_META)); + for (const edge of INFLUENCES) { + if (edge.work?.artist) names.add(edge.work.artist); + walkInfluence(edge.influencedBy, (node) => { + if (node.artist) names.add(node.artist); + if (node.artistMeta?.movement) names.add(node.artist); + }); + } + for (const entry of FAMOUS) names.add(entry.artist); + for (const name of Object.keys(FLAGSHIP_OVERRIDES)) names.add(name); + return [...names].sort((a, b) => a.localeCompare(b)); +} + +function metaFromInfluence(name) { + for (const edge of INFLUENCES) { + let found = null; + if (edge.work?.artist === name && edge.work.artistMeta) found = edge.work.artistMeta; + walkInfluence(edge.influencedBy, (node) => { + if (node.artist === name && node.artistMeta) found = node.artistMeta; + }); + if (found) return found; + } + return null; +} + +function buildArtists() { + const flagships = collectFlagships(); + return collectArtistNames().map((name) => { + const staticMeta = ARTIST_META[name] || {}; + const inflMeta = metaFromInfluence(name) || {}; + const birth = staticMeta.birth ?? inflMeta.birth_year ?? null; + const death = staticMeta.death ?? inflMeta.death_year ?? null; + const movement = staticMeta.movement || inflMeta.movement || 'Realism'; + const wikipedia_title = WIKI_OVERRIDES[name] || inflMeta.wikipedia_title || name; + const painting = flagships.get(name) || FLAGSHIP_OVERRIDES[name] || null; + return { + name, + movement, + birth_year: birth, + death_year: death, + wikipedia_title, + century: inflMeta.century ?? centuryFromBirth(birth), + painting, + }; + }); +} + +module.exports = { + ERAS, + MOVEMENTS, + buildArtists, +}; diff --git a/scripts/seed-wikipedia.js b/scripts/seed-wikipedia.js new file mode 100644 index 0000000..ed0a8f0 --- /dev/null +++ b/scripts/seed-wikipedia.js @@ -0,0 +1,196 @@ +/** + * Seed historical eras, art movements, artists, periods, and flagship paintings. + * Run: npm run seed + * Flags: --force (re-seed), --fetch-images (download portraits and paintings from Wikipedia) + */ +require('dotenv').config(); +const path = require('path'); +const pool = require('../server/db'); +const { ERAS, MOVEMENTS, buildArtists } = require('./seed-catalog-data'); +const { savePaintingImages, saveImageForItem } = require('./image-fetcher'); + +const FORCE = process.argv.includes('--force'); +const FETCH_IMAGES = process.argv.includes('--fetch-images'); +const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images')); + +async function tableCount(table) { + const { rows } = await pool.query(`SELECT COUNT(*)::int AS n FROM ${table}`); + return rows[0].n; +} + +async function clearCatalog() { + await pool.query(` + TRUNCATE TABLE + painting_annotations, + painting_influence_sources, + painting_influences, + paintings, + artist_periods, + artists, + art_movements, + historical_eras + RESTART IDENTITY CASCADE + `); +} + +async function insertEras() { + const eraIds = new Map(); + for (const era of ERAS) { + const { rows } = await pool.query( + `INSERT INTO historical_eras (name, start_year, end_year, start_definite, end_definite, description, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id`, + [era.name, era.start_year, era.end_year, era.start_definite, era.end_definite, era.description, era.sort_order] + ); + eraIds.set(era.name, rows[0].id); + } + return eraIds; +} + +async function insertMovements(eraIds) { + const movementIds = new Map(); + for (const mov of MOVEMENTS) { + const eraId = eraIds.get(mov.era) ?? null; + const { rows } = await pool.query( + `INSERT INTO art_movements (name, start_year, end_year, era_id, description, color) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id`, + [mov.name, mov.start_year, mov.end_year, eraId, mov.description, mov.color] + ); + movementIds.set(mov.name, rows[0].id); + } + return movementIds; +} + +function periodName(artist) { + if (artist.birth_year != null && artist.death_year != null) { + return `${artist.birth_year}–${artist.death_year}`; + } + return 'Main period'; +} + +async function insertArtists(movementIds, artists) { + let inserted = 0; + let paintings = 0; + let skippedPaintings = 0; + let portraitsFetched = 0; + let imagesFetched = 0; + + for (const artist of artists) { + const movementId = movementIds.get(artist.movement); + if (!movementId) { + console.warn(` Unknown movement "${artist.movement}" for ${artist.name} — skipped`); + continue; + } + + const artistRes = await pool.query( + `INSERT INTO artists (name, birth_year, death_year, movement_id, wikipedia_title, century) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id`, + [artist.name, artist.birth_year, artist.death_year, movementId, artist.wikipedia_title, artist.century] + ); + const artistId = artistRes.rows[0].id; + inserted += 1; + + const periodRes = await pool.query( + `INSERT INTO artist_periods (artist_id, name, start_year, end_year, sort_order) + VALUES ($1, $2, $3, $4, 0) + RETURNING id`, + [artistId, periodName(artist), artist.birth_year, artist.death_year] + ); + const periodId = periodRes.rows[0].id; + + if (FETCH_IMAGES) { + try { + const portraitBase = artist.name.replace(/[^a-zA-Z0-9_-]/g, '_'); + const saved = await saveImageForItem(artist.wikipedia_title, 'portraits', portraitBase, IMAGE_DIR); + if (saved.path) { + await pool.query('UPDATE artists SET portrait_path = $1 WHERE id = $2', [saved.path, artistId]); + portraitsFetched += 1; + } + } catch (err) { + console.warn(` portrait: ${artist.name}: ${err.message}`); + } + } + + if (!artist.painting?.title) { + skippedPaintings += 1; + console.warn(` no flagship painting for ${artist.name}`); + continue; + } + + const wikiTitle = artist.painting.wikipedia_title || artist.painting.title; + const paintingRes = await pool.query( + `INSERT INTO paintings (artist_id, period_id, title, year, wikipedia_title, sort_order) + VALUES ($1, $2, $3, $4, $5, 0) + RETURNING id`, + [artistId, periodId, artist.painting.title, artist.painting.year ?? null, wikiTitle] + ); + const paintingId = paintingRes.rows[0].id; + paintings += 1; + + if (FETCH_IMAGES) { + try { + const base = `${artist.name}_${artist.painting.title}`.replace(/[^a-zA-Z0-9_-]/g, '_'); + const saved = await savePaintingImages(wikiTitle, base, IMAGE_DIR, { + artistName: artist.name, + paintingTitle: artist.painting.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, paintingId] + ); + imagesFetched += 1; + } + } catch (err) { + console.warn(` image: ${artist.name} — ${artist.painting.title}: ${err.message}`); + } + } + + console.log(`+ ${artist.name} (${artist.movement}) — ${artist.painting.title}`); + } + + return { inserted, paintings, skippedPaintings, portraitsFetched, imagesFetched }; +} + +async function main() { + const existing = await tableCount('artists'); + if (existing > 0 && !FORCE) { + console.log(`Catalog already has ${existing} artists. Use --force to re-seed.`); + return; + } + + if (FORCE && existing > 0) { + console.log('Clearing existing catalog…'); + await clearCatalog(); + } + + const artists = buildArtists(); + console.log(`Seeding ${ERAS.length} eras, ${MOVEMENTS.length} movements, ${artists.length} artists…`); + + const eraIds = await insertEras(); + const movementIds = await insertMovements(eraIds); + const stats = await insertArtists(movementIds, artists); + + console.log('\nSeed complete.'); + console.log(` Artists: ${stats.inserted}`); + console.log(` Paintings: ${stats.paintings}${stats.skippedPaintings ? ` (${stats.skippedPaintings} without flagship)` : ''}`); + if (FETCH_IMAGES) { + console.log(` Portraits downloaded: ${stats.portraitsFetched}`); + console.log(` Painting images: ${stats.imagesFetched}`); + } else { + console.log(' Run with --fetch-images to download artwork files, or npm run fetch-images later.'); + } + console.log('\nNext steps:'); + console.log(' npm run fetch-artist-bios'); + console.log(' npm run expand-catalog'); + console.log(' npm run update-influences'); +} + +main() + .then(() => pool.end()) + .catch((err) => { + console.error('Seed failed:', err.message); + pool.end().finally(() => process.exit(1)); + });