Add PainterPalette integration, canonical DB schema, and influence UI fixes.
Integrate Inputs/PainterPalette.csv for artist metadata and influence links, add db/schema.sql with server/migrate.js, letterbox influence panel thumbnails, and refresh Titian/Pontormo images. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
4eead54062
commit
b2cae284ac
@@ -0,0 +1,27 @@
|
||||
require('dotenv').config();
|
||||
const pool = require('../server/db');
|
||||
const { loadPainterPalette, findPaletteRow } = require('./painter-palette-lib');
|
||||
|
||||
async function main() {
|
||||
const palette = loadPainterPalette();
|
||||
const { rows: dbArtists } = await pool.query('SELECT name FROM artists ORDER BY name');
|
||||
|
||||
let matched = 0;
|
||||
const unmatched = [];
|
||||
for (const a of dbArtists) {
|
||||
if (findPaletteRow(a.name, palette)) matched += 1;
|
||||
else unmatched.push(a.name);
|
||||
}
|
||||
|
||||
console.log('CSV rows:', palette.rowCount);
|
||||
console.log('DB artists:', dbArtists.length);
|
||||
console.log('Matched to PainterPalette:', matched, '/', dbArtists.length);
|
||||
if (unmatched.length) console.log('Unmatched:', unmatched.join('; '));
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pool = require('../server/db');
|
||||
|
||||
function csvEscape(value) {
|
||||
const v = value == null ? '' : String(value);
|
||||
if (/[",\n\r]/.test(v)) return `"${v.replace(/"/g, '""')}"`;
|
||||
return v;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT a.name AS artist, p.title AS painting, p.year
|
||||
FROM paintings p
|
||||
JOIN artists a ON a.id = p.artist_id
|
||||
ORDER BY a.name, p.year NULLS LAST, p.title
|
||||
`);
|
||||
|
||||
const outDir = path.join(__dirname, '..', 'Output');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const header = 'artist,painting,year';
|
||||
const lines = rows.map((r) =>
|
||||
[csvEscape(r.artist), csvEscape(r.painting), csvEscape(r.year ?? '')].join(',')
|
||||
);
|
||||
const file = path.join(outDir, 'paintings.csv');
|
||||
fs.writeFileSync(file, `${[header, ...lines].join('\n')}\n`, 'utf8');
|
||||
console.log(`Wrote ${rows.length} rows to ${file}`);
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Integrate Inputs/PainterPalette.csv into existing gallery artists:
|
||||
* - Enrich artists.palette_metadata (+ fill missing birth/death years)
|
||||
* - Insert artist/movement influence sources from Influencedby, Teachers, Influencedon, Pupils
|
||||
*
|
||||
* Usage:
|
||||
* npm run migrate:artist-palette
|
||||
* npm run import-painter-palette # metadata + influences
|
||||
* npm run import-painter-palette -- --dry-run
|
||||
* npm run import-painter-palette -- --metadata-only
|
||||
* npm run import-painter-palette -- --influences-only
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const pool = require('../server/db');
|
||||
const {
|
||||
loadPainterPalette,
|
||||
findPaletteRow,
|
||||
buildArtistResolver,
|
||||
collectInfluenceSources,
|
||||
} = require('./painter-palette-lib');
|
||||
const { loadMovements, resolveAndInsertSource } = require('./influence-resolver');
|
||||
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
const METADATA_ONLY = process.argv.includes('--metadata-only');
|
||||
const INFLUENCES_ONLY = process.argv.includes('--influences-only');
|
||||
|
||||
const PALETTE_EDGE = {
|
||||
notes: 'Artist relationship from PainterPalette dataset (WikiArt / Art500k / Wikidata enrichment).',
|
||||
source: 'PainterPalette.csv',
|
||||
source_author: 'PainterPalette',
|
||||
confidence: 'discovered',
|
||||
discovered_via: 'painter-palette',
|
||||
};
|
||||
|
||||
function roleNote(role) {
|
||||
if (role === 'teacher') return 'Listed as teacher in PainterPalette.';
|
||||
if (role === 'pupil') return 'Listed as pupil in PainterPalette.';
|
||||
if (role === 'influenced_on') return 'Listed as influenced by this artist in PainterPalette.';
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tableCheck = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_name = 'artists' AND column_name = 'palette_metadata'
|
||||
) AS ok
|
||||
`);
|
||||
if (!tableCheck.rows[0]?.ok) {
|
||||
console.error('Run npm run migrate:artist-palette first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const palette = loadPainterPalette();
|
||||
console.log(`Loaded PainterPalette: ${palette.rowCount} painters`);
|
||||
|
||||
const { rows: dbArtists } = await pool.query(`
|
||||
SELECT a.id, a.name, a.birth_year, a.death_year
|
||||
FROM artists a
|
||||
ORDER BY a.name
|
||||
`);
|
||||
|
||||
const { rows: paintings } = await pool.query(`
|
||||
SELECT p.id, p.year, p.artist_id, 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.id
|
||||
`);
|
||||
|
||||
const paintingsByArtist = new Map();
|
||||
for (const p of paintings) {
|
||||
if (!paintingsByArtist.has(p.artist_id)) paintingsByArtist.set(p.artist_id, []);
|
||||
paintingsByArtist.get(p.artist_id).push(p);
|
||||
}
|
||||
|
||||
const resolver = buildArtistResolver(dbArtists);
|
||||
const movementsByName = await loadMovements(pool);
|
||||
|
||||
const stats = {
|
||||
matched: 0,
|
||||
metadataUpdated: 0,
|
||||
yearsFilled: 0,
|
||||
influencesAdded: 0,
|
||||
influencesSkipped: 0,
|
||||
influencesFailed: 0,
|
||||
unresolved: new Set(),
|
||||
};
|
||||
|
||||
const matchedPairs = [];
|
||||
for (const artist of dbArtists) {
|
||||
const row = findPaletteRow(artist.name, palette);
|
||||
if (row) {
|
||||
stats.matched += 1;
|
||||
matchedPairs.push({ artist, row });
|
||||
}
|
||||
}
|
||||
console.log(`Matched ${stats.matched} / ${dbArtists.length} gallery artists to PainterPalette`);
|
||||
|
||||
if (!INFLUENCES_ONLY) {
|
||||
for (const { artist, row } of matchedPairs) {
|
||||
const meta = row.metadata;
|
||||
const updates = [];
|
||||
const params = [];
|
||||
let n = 1;
|
||||
|
||||
params.push(JSON.stringify(meta));
|
||||
updates.push(`palette_metadata = $${n++}`);
|
||||
|
||||
if (!artist.birth_year && meta.birth_year) {
|
||||
updates.push(`birth_year = $${n++}`);
|
||||
params.push(Math.round(meta.birth_year));
|
||||
stats.yearsFilled += 1;
|
||||
}
|
||||
if (!artist.death_year && meta.death_year) {
|
||||
updates.push(`death_year = $${n++}`);
|
||||
params.push(Math.round(meta.death_year));
|
||||
stats.yearsFilled += 1;
|
||||
}
|
||||
|
||||
params.push(artist.id);
|
||||
const sql = `UPDATE artists SET ${updates.join(', ')} WHERE id = $${n}`;
|
||||
if (DRY_RUN) {
|
||||
console.log(`[dry-run] metadata ${artist.name} ← ${row.artist}`);
|
||||
} else {
|
||||
await pool.query(sql, params);
|
||||
stats.metadataUpdated += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!METADATA_ONLY) {
|
||||
async function applySourceToPaintings(paintingList, sourceRef, edge, subjectArtistId) {
|
||||
if (!paintingList?.length) return;
|
||||
for (const p of paintingList) {
|
||||
if (DRY_RUN) {
|
||||
stats.influencesAdded += 1;
|
||||
continue;
|
||||
}
|
||||
const result = await resolveAndInsertSource(
|
||||
pool,
|
||||
{ id: p.id, year: p.year, artist_id: subjectArtistId },
|
||||
sourceRef,
|
||||
edge,
|
||||
movementsByName,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
);
|
||||
if (result.inserted) stats.influencesAdded += 1;
|
||||
else if (result.ok) stats.influencesSkipped += 1;
|
||||
else if (result.reason !== 'self' && result.reason !== 'self artist') {
|
||||
stats.influencesFailed += 1;
|
||||
} else stats.influencesSkipped += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { artist, row } of matchedPairs) {
|
||||
const subjectPaintings = paintingsByArtist.get(artist.id) || [];
|
||||
if (!subjectPaintings.length) continue;
|
||||
|
||||
const sources = collectInfluenceSources(row);
|
||||
for (const { raw, role } of sources) {
|
||||
const resolved = resolver.resolve(raw);
|
||||
if (!resolved) {
|
||||
if (!isNoise(raw)) stats.unresolved.add(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
const extraNote = roleNote(role);
|
||||
const edge = { ...PALETTE_EDGE, notes: extraNote ? `${PALETTE_EDGE.notes} ${extraNote}` : PALETTE_EDGE.notes };
|
||||
|
||||
if (role === 'influenced_by' || role === 'teacher') {
|
||||
if (resolved.type === 'artist') {
|
||||
await applySourceToPaintings(
|
||||
subjectPaintings,
|
||||
{ type: 'artist', artist: resolved.artist },
|
||||
edge,
|
||||
artist.id
|
||||
);
|
||||
} else if (resolved.type === 'movement') {
|
||||
await applySourceToPaintings(
|
||||
subjectPaintings,
|
||||
{ type: 'movement', movement: resolved.movement },
|
||||
edge,
|
||||
artist.id
|
||||
);
|
||||
}
|
||||
} else if (role === 'influenced_on' || role === 'pupil') {
|
||||
if (resolved.type !== 'artist') continue;
|
||||
const targetArtist = dbArtists.find((a) => a.id === resolved.id);
|
||||
if (!targetArtist) continue;
|
||||
const targetPaintings = paintingsByArtist.get(resolved.id) || [];
|
||||
const reverseEdge = {
|
||||
...edge,
|
||||
notes: `${PALETTE_EDGE.notes} Reverse link: ${artist.name} listed as influence on ${resolved.artist}.`,
|
||||
};
|
||||
await applySourceToPaintings(
|
||||
targetPaintings,
|
||||
{ type: 'artist', artist: artist.name },
|
||||
reverseEdge,
|
||||
resolved.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { rows: totalSources } = await pool.query(
|
||||
'SELECT COUNT(*)::int AS n FROM painting_influence_sources WHERE discovered_via = $1',
|
||||
['painter-palette']
|
||||
);
|
||||
|
||||
console.log('\n--- Summary ---');
|
||||
if (!INFLUENCES_ONLY) {
|
||||
console.log(`Metadata updated: ${stats.metadataUpdated}${DRY_RUN ? ' (dry-run)' : ''}`);
|
||||
console.log(`Birth/death years filled: ${stats.yearsFilled}`);
|
||||
}
|
||||
if (!METADATA_ONLY) {
|
||||
console.log(
|
||||
`Influence rows attempted: ${stats.influencesAdded} inserted, ${stats.influencesSkipped} skipped, ${stats.influencesFailed} failed${DRY_RUN ? ' (dry-run counts inserts as attempts)' : ''}`
|
||||
);
|
||||
console.log(`Unresolved influence names (sample): ${[...stats.unresolved].slice(0, 15).join('; ')}`);
|
||||
}
|
||||
console.log(`Total painter-palette influence sources in DB: ${totalSources[0].n}`);
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
function isNoise(name) {
|
||||
const { isNoiseInfluenceName } = require('./painter-palette-lib');
|
||||
return isNoiseInfluenceName(name);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pool = require('../server/db');
|
||||
|
||||
async function main() {
|
||||
const sql = fs.readFileSync(path.join(__dirname, '../db/migrate-artist-palette.sql'), 'utf8');
|
||||
await pool.query(sql);
|
||||
const { rows } = await pool.query(`
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'artists' AND column_name = 'palette_metadata'
|
||||
`);
|
||||
console.log(rows.length ? 'artists.palette_metadata ready' : 'migration may have failed');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Shared PainterPalette.csv parsing, name matching, and influence cleaning.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CSV_PATH = path.join(__dirname, '../Inputs/PainterPalette.csv');
|
||||
|
||||
/** Gallery DB name → PainterPalette.csv artist column */
|
||||
const DB_TO_PALETTE_NAME = {
|
||||
Bronzino: 'Agnolo Bronzino',
|
||||
Pontormo: 'Jacopo Pontormo',
|
||||
'Ivan Klyun': 'Ivan Kliun',
|
||||
'Henri Privat-Livemont': 'Henri Privat Livemont',
|
||||
Zeuxis: 'Zeuxis',
|
||||
Parrhasius: 'Parrhasius',
|
||||
'Theophanes the Greek': 'Theophanes the Greek',
|
||||
'J. M. W. Turner': 'J.M.W. Turner',
|
||||
'Édouard Manet': 'Edouard Manet',
|
||||
'Paul Cézanne': 'Paul Cezanne',
|
||||
'Eugène Delacroix': 'Eugene Delacroix',
|
||||
'François Boucher': 'Francois Boucher',
|
||||
'Jean-Auguste-Dominique Ingres': 'Jean Auguste Dominique Ingres',
|
||||
'Jean-Baptiste-Camille Corot': 'Jean-Baptiste-Camille Corot',
|
||||
'Wassily Kandinsky': 'Wassily Kandinsky',
|
||||
'Kazimir Malevich': 'Kazimir Malevich',
|
||||
'Willem de Kooning': 'Willem de Kooning',
|
||||
'René Magritte': 'Rene Magritte',
|
||||
};
|
||||
|
||||
/** PainterPalette movement/style label → gallery art_movements.name */
|
||||
const MOVEMENT_ALIASES = new Map(
|
||||
Object.entries({
|
||||
'byzantine art': 'Byzantine',
|
||||
byzantine: 'Byzantine',
|
||||
impressionism: 'Impressionism',
|
||||
'post-impressionism': 'Post-Impressionism',
|
||||
'neo-impressionism': 'Post-Impressionism',
|
||||
cubism: 'Cubism',
|
||||
'analytical cubism': 'Cubism',
|
||||
'synthetic cubism': 'Cubism',
|
||||
surrealism: 'Surrealism',
|
||||
symbolism: 'Symbolism',
|
||||
fauvism: 'Fauvism',
|
||||
expressionism: 'Expressionism',
|
||||
'abstract expressionism': 'Abstract Expressionism',
|
||||
futurism: 'Futurism',
|
||||
dada: 'Dada',
|
||||
constructivism: 'Constructivism',
|
||||
suprematism: 'Suprematism',
|
||||
'pop art': 'Pop Art',
|
||||
realism: 'Realism',
|
||||
romanticism: 'Romanticism',
|
||||
baroque: 'Baroque',
|
||||
rococo: 'Rococo',
|
||||
neoclassicism: 'Neoclassicism',
|
||||
'art nouveau': 'Art Nouveau',
|
||||
'art nouveau modern': 'Art Nouveau',
|
||||
gothic: 'Gothic',
|
||||
'high renaissance': 'High Renaissance',
|
||||
'early renaissance': 'Early Renaissance',
|
||||
'northern renaissance': 'Northern Renaissance',
|
||||
mannerism: 'Mannerism',
|
||||
'mannerism late renaissance': 'Mannerism',
|
||||
'ancient greek roman': 'Ancient Greek & Roman',
|
||||
'ancient greek & roman': 'Ancient Greek & Roman',
|
||||
})
|
||||
);
|
||||
|
||||
function parseCSVLine(line) {
|
||||
const out = [];
|
||||
let cur = '';
|
||||
let q = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const c = line[i];
|
||||
if (c === '"') {
|
||||
q = !q;
|
||||
continue;
|
||||
}
|
||||
if (c === ',' && !q) {
|
||||
out.push(cur);
|
||||
cur = '';
|
||||
continue;
|
||||
}
|
||||
cur += c;
|
||||
}
|
||||
out.push(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
return (name || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseNameList(field) {
|
||||
if (!field || !String(field).trim()) return [];
|
||||
return String(field)
|
||||
.split(',')
|
||||
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function isNoiseInfluenceName(name) {
|
||||
const n = name.trim();
|
||||
if (!n || n.length < 2) return true;
|
||||
if (/\.(jpg|jpeg|png|gif)\b/i.test(n)) return true;
|
||||
if (/^Artists\d/i.test(n)) return true;
|
||||
if (/\b(museum|gallery|tretyakov|hermitage|louvre|national museum)\b/i.test(n)) return true;
|
||||
if (/^(famous-people|male-portraits|female-portraits|winter|gardens-and-parks|rivers-and)/i.test(n)) return true;
|
||||
if (/^[a-z0-9]+(-[a-z0-9]+){2,}$/.test(n) && !/\s/.test(n)) return true;
|
||||
if (/^\d+(\.\d+)?\s*x\s*\d+(\.\d+)?\s*cm$/i.test(n)) return true;
|
||||
if (/^[\d\s.x×cm]+$/i.test(n)) return true;
|
||||
if (/^[^\x00-\x7F]+$/.test(n) && n.length < 40) return true;
|
||||
if (n.length > 120) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function paletteRowToMetadata(row, idx) {
|
||||
const num = (v) => {
|
||||
if (v == null || v === '') return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
return {
|
||||
source: 'PainterPalette.csv',
|
||||
nationality: row[idx.Nationality] || null,
|
||||
citizenship: row[idx.citizenship] || null,
|
||||
gender: row[idx.gender] || null,
|
||||
styles: row[idx.styles] || null,
|
||||
movement: row[idx.movement] || row[idx.ArtMovement] || null,
|
||||
art500k_movements: row[idx.Art500k_Movements] || null,
|
||||
birth_place: row[idx.birth_place] || null,
|
||||
death_place: row[idx.death_place] || null,
|
||||
birth_year: num(row[idx.birth_year]),
|
||||
death_year: num(row[idx.death_year]),
|
||||
first_year: num(row[idx.FirstYear]),
|
||||
last_year: num(row[idx.LastYear]),
|
||||
wikiart_pictures_count: num(row[idx.wikiart_pictures_count]),
|
||||
occupations: row[idx.occupations] || null,
|
||||
painting_school: row[idx.PaintingSchool] || null,
|
||||
styles_extended: row[idx.styles_extended] || null,
|
||||
influenced_by_raw: row[idx.Influencedby] || null,
|
||||
influenced_on_raw: row[idx.Influencedon] || null,
|
||||
teachers_raw: row[idx.Teachers] || null,
|
||||
pupils_raw: row[idx.Pupils] || null,
|
||||
};
|
||||
}
|
||||
|
||||
function loadPainterPalette() {
|
||||
const lines = fs.readFileSync(CSV_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
|
||||
const hdr = parseCSVLine(lines[0]);
|
||||
const idx = Object.fromEntries(hdr.map((h, i) => [h, i]));
|
||||
|
||||
const byNorm = new Map();
|
||||
const byExact = new Map();
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const row = parseCSVLine(lines[i]);
|
||||
const artist = row[idx.artist]?.trim();
|
||||
if (!artist) continue;
|
||||
const entry = { artist, row, idx, metadata: paletteRowToMetadata(row, idx) };
|
||||
byExact.set(artist.toLowerCase(), entry);
|
||||
byNorm.set(normalizeName(artist), entry);
|
||||
}
|
||||
|
||||
return { idx, byNorm, byExact, rowCount: lines.length - 1 };
|
||||
}
|
||||
|
||||
function findPaletteRow(dbName, palette) {
|
||||
const alias = DB_TO_PALETTE_NAME[dbName];
|
||||
if (alias) {
|
||||
const hit = palette.byExact.get(alias.toLowerCase());
|
||||
if (hit) return hit;
|
||||
}
|
||||
const exact = palette.byExact.get(dbName.toLowerCase());
|
||||
if (exact) return exact;
|
||||
const norm = palette.byNorm.get(normalizeName(dbName));
|
||||
if (norm) return norm;
|
||||
|
||||
const dbNorm = normalizeName(dbName);
|
||||
const parts = dbNorm.split(' ').filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const last = parts[parts.length - 1];
|
||||
const candidates = [];
|
||||
for (const [n, entry] of palette.byNorm) {
|
||||
if (n.endsWith(` ${last}`) || n === last) candidates.push(entry);
|
||||
}
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildArtistResolver(dbArtists) {
|
||||
const byNorm = new Map();
|
||||
const byLast = new Map();
|
||||
|
||||
for (const a of dbArtists) {
|
||||
const norm = normalizeName(a.name);
|
||||
if (!byNorm.has(norm)) byNorm.set(norm, a);
|
||||
const parts = norm.split(' ').filter(Boolean);
|
||||
const last = parts[parts.length - 1];
|
||||
if (!byLast.has(last)) byLast.set(last, []);
|
||||
byLast.get(last).push(a);
|
||||
}
|
||||
|
||||
function resolve(rawName) {
|
||||
const name = rawName.trim();
|
||||
if (!name || isNoiseInfluenceName(name)) return null;
|
||||
|
||||
const alias = DB_TO_PALETTE_NAME[name] || name;
|
||||
const norm = normalizeName(alias);
|
||||
if (byNorm.has(norm)) return { type: 'artist', artist: byNorm.get(norm).name, id: byNorm.get(norm).id };
|
||||
|
||||
const parts = norm.split(' ').filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const last = parts[parts.length - 1];
|
||||
const cands = (byLast.get(last) || []).filter((a) => {
|
||||
const an = normalizeName(a.name);
|
||||
return parts.every((p) => an.includes(p));
|
||||
});
|
||||
if (cands.length === 1) return { type: 'artist', artist: cands[0].name, id: cands[0].id };
|
||||
}
|
||||
|
||||
const movementKey = norm.replace(/\s+/g, ' ');
|
||||
if (MOVEMENT_ALIASES.has(movementKey)) {
|
||||
return { type: 'movement', movement: MOVEMENT_ALIASES.get(movementKey) };
|
||||
}
|
||||
for (const [key, movement] of MOVEMENT_ALIASES) {
|
||||
if (movementKey === key || movementKey.startsWith(`${key} `)) {
|
||||
return { type: 'movement', movement };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return { resolve };
|
||||
}
|
||||
|
||||
function collectInfluenceSources(paletteEntry) {
|
||||
const { row, idx } = paletteEntry;
|
||||
const sources = [];
|
||||
const seen = new Set();
|
||||
|
||||
const addField = (field, role) => {
|
||||
for (const raw of parseNameList(row[idx[field]])) {
|
||||
const key = `${role}:${normalizeName(raw)}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
sources.push({ raw, role });
|
||||
}
|
||||
};
|
||||
|
||||
addField('Teachers', 'teacher');
|
||||
addField('Influencedby', 'influenced_by');
|
||||
addField('Influencedon', 'influenced_on');
|
||||
addField('Pupils', 'pupil');
|
||||
return sources;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CSV_PATH,
|
||||
DB_TO_PALETTE_NAME,
|
||||
MOVEMENT_ALIASES,
|
||||
parseCSVLine,
|
||||
normalizeName,
|
||||
parseNameList,
|
||||
isNoiseInfluenceName,
|
||||
paletteRowToMetadata,
|
||||
loadPainterPalette,
|
||||
findPaletteRow,
|
||||
buildArtistResolver,
|
||||
collectInfluenceSources,
|
||||
};
|
||||
Reference in New Issue
Block a user