Add seed scripts, proxy Vite on public domain, and seed painting images.
Restore seed-wikipedia.js and seed-catalog-data.js so npm run setup works on a fresh clone. Point nginx at the Vite dev client and allow the public hostname in Vite config.
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
After Width: | Height: | Size: 968 KiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 830 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||