open-museum-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ import dynastiesData from './data/dynasties.json' with { type: 'json' };
2
+ const dynasties = dynastiesData;
3
+ const flatDynasties = (() => {
4
+ const out = {};
5
+ const seen = new Set();
6
+ for (const culture of Object.keys(dynasties)) {
7
+ for (const [period, range] of Object.entries(dynasties[culture])) {
8
+ const key = period.toLowerCase();
9
+ if (seen.has(key)) {
10
+ console.warn(`[open-museum-mcp] dynasty table: duplicate period "${key}" across cultures; last definition wins`);
11
+ }
12
+ seen.add(key);
13
+ out[key] = range;
14
+ }
15
+ }
16
+ return out;
17
+ })();
18
+ const ROMAN = {
19
+ i: 1, ii: 2, iii: 3, iv: 4, v: 5, vi: 6, vii: 7, viii: 8, ix: 9, x: 10,
20
+ xi: 11, xii: 12, xiii: 13, xiv: 14, xv: 15, xvi: 16, xvii: 17, xviii: 18,
21
+ xix: 19, xx: 20, xxi: 21,
22
+ };
23
+ const ORDINAL_WORDS = {
24
+ first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, seventh: 7,
25
+ eighth: 8, ninth: 9, tenth: 10, eleventh: 11, twelfth: 12, thirteenth: 13,
26
+ fourteenth: 14, fifteenth: 15, sixteenth: 16, seventeenth: 17,
27
+ eighteenth: 18, nineteenth: 19, twentieth: 20, 'twenty-first': 21,
28
+ };
29
+ function ordinalToNumber(token) {
30
+ const lower = token.toLowerCase();
31
+ // Check the word dictionary BEFORE stripping suffixes — otherwise hyphenated
32
+ // forms like "twenty-first" get mangled into "twenty-fir" by the suffix
33
+ // regex and never match.
34
+ if (ORDINAL_WORDS[lower] !== undefined)
35
+ return ORDINAL_WORDS[lower];
36
+ const stripped = lower.replace(/(st|nd|rd|th)$/, '');
37
+ if (/^\d+$/.test(stripped))
38
+ return parseInt(stripped, 10);
39
+ if (ROMAN[stripped] !== undefined)
40
+ return ROMAN[stripped];
41
+ return ORDINAL_WORDS[stripped] ?? null;
42
+ }
43
+ // 19th century CE = 1801–1900 (no year zero, century N spans years
44
+ // (N-1)*100+1 to N*100). qualifier divides the span into thirds; floor()
45
+ // accepts that the 99-year span doesn't divide evenly — close enough for
46
+ // catalog metadata and matches museum convention.
47
+ function centuryRange(n, era, qualifier) {
48
+ let start, end;
49
+ if (era === 'ce') {
50
+ start = (n - 1) * 100 + 1;
51
+ end = n * 100;
52
+ }
53
+ else {
54
+ end = -((n - 1) * 100 + 1);
55
+ start = -(n * 100);
56
+ }
57
+ const span = end - start;
58
+ if (qualifier === 'early') {
59
+ return { yearStart: start, yearEnd: start + Math.floor(span / 3) };
60
+ }
61
+ if (qualifier === 'mid' || qualifier === 'middle') {
62
+ return {
63
+ yearStart: start + Math.floor(span / 3),
64
+ yearEnd: end - Math.floor(span / 3),
65
+ };
66
+ }
67
+ if (qualifier === 'late') {
68
+ return { yearStart: end - Math.floor(span / 3), yearEnd: end };
69
+ }
70
+ return { yearStart: start, yearEnd: end };
71
+ }
72
+ // True only when the string carries BOTH a BCE marker and a CE marker —
73
+ // signals a cross-era range that tryRangeRegex must defer on so
74
+ // tryCrossEraRange can handle it correctly.
75
+ function hasMixedEras(s) {
76
+ const hasBce = /\b(b\.?c\.?e?\.?|bc)\b/i.test(s);
77
+ if (!hasBce)
78
+ return false;
79
+ return /\bce\b/i.test(s) || /\bc\.e\./i.test(s);
80
+ }
81
+ function tryCrossEraRange(s) {
82
+ const m = s.match(/(\d{1,5})\s*(?:b\.?c\.?e?\.?|bc)\s*[-–]\s*(\d{1,5})\s*(?:c\.?e\.?|ce)/i);
83
+ if (m) {
84
+ return {
85
+ yearStart: -parseInt(m[1], 10),
86
+ yearEnd: parseInt(m[2], 10),
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+ // Numeric range with optional trailing BCE marker. Bare digits separated by
92
+ // hyphen/en-dash. Note: ordinal-century inputs like "14th-15th century" are
93
+ // NOT matched here because the regex requires the dash to immediately follow
94
+ // digits — the "th" between "14" and "-" breaks the match. Don't relax the
95
+ // regex without locking down that invariant in tests; tryCenturyRange owns
96
+ // ordinal-century parsing.
97
+ function tryRangeRegex(s) {
98
+ if (hasMixedEras(s))
99
+ return null;
100
+ const m = s.match(/(-?\d{1,5})\s*[-–]\s*(-?\d{1,5})\s*(b\.?c\.?e?\.?|bc)?/i);
101
+ if (m) {
102
+ const firstStr = m[1];
103
+ const secondStr = m[2];
104
+ let a = parseInt(firstStr, 10);
105
+ let b = parseInt(secondStr, 10);
106
+ const firstIsCleanFourDigit = !firstStr.startsWith('-') && firstStr.length === 4;
107
+ const secondIsShortSuffix = !secondStr.startsWith('-') && (secondStr.length === 1 || secondStr.length === 2);
108
+ // Short-suffix forms expand the second number by reusing digits from the
109
+ // first: "1820-5" → 1820–1825 (decade rollover), "1899–05" → 1899–1905
110
+ // (century rollover). When the candidate would land before the start
111
+ // year, bump it forward by the rollover unit.
112
+ if (!m[3] && firstIsCleanFourDigit && secondIsShortSuffix) {
113
+ const decade = Math.floor(a / 10) * 10;
114
+ const century = Math.floor(a / 100) * 100;
115
+ const candidateDecade = decade + b;
116
+ const candidateCentury = century + b;
117
+ let resolved;
118
+ if (secondStr.length === 1) {
119
+ resolved = candidateDecade < a ? candidateDecade + 10 : candidateDecade;
120
+ }
121
+ else {
122
+ resolved = candidateCentury < a ? candidateCentury + 100 : candidateCentury;
123
+ }
124
+ return { yearStart: a, yearEnd: resolved };
125
+ }
126
+ if (m[3]) {
127
+ a = -Math.abs(a);
128
+ b = -Math.abs(b);
129
+ }
130
+ return { yearStart: Math.min(a, b), yearEnd: Math.max(a, b) };
131
+ }
132
+ return null;
133
+ }
134
+ function tryCenturyRange(s) {
135
+ const m = s.match(/([\w-]+)\s*[-–]\s*([\w-]+)\s*(?:-|\s)?\s*century\s*(b\.?c\.?e?\.?|bc)?/i);
136
+ if (!m)
137
+ return null;
138
+ const startN = ordinalToNumber(m[1]);
139
+ const endN = ordinalToNumber(m[2]);
140
+ if (startN === null || endN === null)
141
+ return null;
142
+ const era = m[3] ? 'bce' : 'ce';
143
+ const startRange = centuryRange(startN, era);
144
+ const endRange = centuryRange(endN, era);
145
+ return {
146
+ yearStart: Math.min(startRange.yearStart, endRange.yearStart),
147
+ yearEnd: Math.max(startRange.yearEnd, endRange.yearEnd),
148
+ };
149
+ }
150
+ function tryDecade(s) {
151
+ const m = s.match(/(?<![\d-])(\d{3,4})s\b/i);
152
+ if (m) {
153
+ const y = parseInt(m[1], 10);
154
+ return { yearStart: y, yearEnd: y + 9 };
155
+ }
156
+ return null;
157
+ }
158
+ function trySingleYear(s) {
159
+ const bce = s.match(/(\d{1,5})\s*(b\.?c\.?e?\.?|bc)\b/i);
160
+ if (bce) {
161
+ const y = -parseInt(bce[1], 10);
162
+ return { yearStart: y, yearEnd: y };
163
+ }
164
+ const ce = s.match(/(?<!\w)(\d{1,5})\s*(c\.?e\.?|ce)\b/i);
165
+ if (ce) {
166
+ const y = parseInt(ce[1], 10);
167
+ return { yearStart: y, yearEnd: y };
168
+ }
169
+ const circa = s.match(/(?:c\.?|ca\.?|circa|approximately|around|about)\s*(-?\d{1,5})/i);
170
+ if (circa) {
171
+ const y = parseInt(circa[1], 10);
172
+ return { yearStart: y - 5, yearEnd: y + 5 };
173
+ }
174
+ const exact = s.match(/(?<![\d-])(\d{3,4})(?![\d-])/);
175
+ if (exact) {
176
+ const y = parseInt(exact[1], 10);
177
+ return { yearStart: y, yearEnd: y };
178
+ }
179
+ return null;
180
+ }
181
+ function tryCentury(s) {
182
+ const qual = s.match(/\b(early|mid|middle|late)\b/i);
183
+ const qualifier = qual ? qual[1].toLowerCase() : undefined;
184
+ const stripped = s.replace(/\b(early|mid|middle|late)\b[\s\-]*/i, ' ').trim();
185
+ const m = stripped.match(/([\w-]+)\s*(?:-|\s)?\s*century\s*(b\.?c\.?e?\.?|bc)?/i);
186
+ if (!m)
187
+ return null;
188
+ const num = ordinalToNumber(m[1]);
189
+ if (num === null)
190
+ return null;
191
+ const era = m[2] ? 'bce' : 'ce';
192
+ return centuryRange(num, era, qualifier);
193
+ }
194
+ function tryDynasty(s) {
195
+ const lower = s.toLowerCase();
196
+ const sorted = Object.keys(flatDynasties).sort((a, b) => b.length - a.length);
197
+ for (const period of sorted) {
198
+ if (lower.includes(period)) {
199
+ const [start, end] = flatDynasties[period];
200
+ const qual = lower.match(/\b(early|mid|middle|late)\b/);
201
+ const qualifier = qual ? qual[1] : undefined;
202
+ const span = end - start;
203
+ if (qualifier === 'early') {
204
+ return { yearStart: start, yearEnd: start + Math.floor(span / 3) };
205
+ }
206
+ if (qualifier === 'mid' || qualifier === 'middle') {
207
+ return {
208
+ yearStart: start + Math.floor(span / 3),
209
+ yearEnd: end - Math.floor(span / 3),
210
+ };
211
+ }
212
+ if (qualifier === 'late') {
213
+ return { yearStart: end - Math.floor(span / 3), yearEnd: end };
214
+ }
215
+ return { yearStart: start, yearEnd: end };
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ /**
221
+ * Parse a museum-supplied display date into a {yearStart, yearEnd} range.
222
+ *
223
+ * Strategies are tried in this exact order — earlier strategies win:
224
+ * 1. cross-era range ("500 BCE – 50 CE")
225
+ * 2. numeric range ("1820–1830", "1820-5", "1899–05")
226
+ * 3. ordinal-century range ("14th-15th century")
227
+ * 4. ordinal century with optional early/mid/late qualifier
228
+ * 5. decade ("1820s")
229
+ * 6. single year ("1888", "ca. 1820", "500 BCE")
230
+ * 7. dynasty/period lookup (longest key first to avoid prefix shadowing)
231
+ *
232
+ * Returns {null, null} when nothing matches — never guesses. BCE is encoded
233
+ * as negative integers so range arithmetic Just Works.
234
+ */
235
+ export function parseDisplayDate(input) {
236
+ if (!input || typeof input !== 'string') {
237
+ return { yearStart: null, yearEnd: null };
238
+ }
239
+ const s = input.trim();
240
+ if (!s)
241
+ return { yearStart: null, yearEnd: null };
242
+ const cross = tryCrossEraRange(s);
243
+ if (cross)
244
+ return cross;
245
+ const range = tryRangeRegex(s);
246
+ if (range)
247
+ return range;
248
+ const centuryRangeMatch = tryCenturyRange(s);
249
+ if (centuryRangeMatch)
250
+ return centuryRangeMatch;
251
+ const century = tryCentury(s);
252
+ if (century)
253
+ return century;
254
+ const decade = tryDecade(s);
255
+ if (decade)
256
+ return decade;
257
+ const single = trySingleYear(s);
258
+ if (single)
259
+ return single;
260
+ const dynasty = tryDynasty(s);
261
+ if (dynasty)
262
+ return dynasty;
263
+ return { yearStart: null, yearEnd: null };
264
+ }
265
+ //# sourceMappingURL=dateParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dateParser.js","sourceRoot":"","sources":["../src/dateParser.ts"],"names":[],"mappings":"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"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Artwork } from './types.js';
2
+ export interface CacheConfig {
3
+ path: string;
4
+ }
5
+ /**
6
+ * SQLite-backed cache for normalized artworks and search-result IDs.
7
+ *
8
+ * TTL contract:
9
+ * - `objects` rows live for {@link OBJECT_TTL_DAYS} days. Artworks don't
10
+ * change once accessioned, so the long TTL keeps cold-cache HTTP cost low.
11
+ * - `query_cache` rows live for {@link QUERY_TTL_DAYS} days. Museums add
12
+ * new open-access objects regularly, so queries refresh on a shorter cycle.
13
+ *
14
+ * Stale rows are pruned on construction (cheap bulk DELETE) so a long-lived
15
+ * client doesn't accumulate dead rows indefinitely. Reads also treat expired
16
+ * rows as misses; the next write overwrites the row via ON CONFLICT.
17
+ *
18
+ * The DB file is created with mode 0o600. Parameterized statements only —
19
+ * no string-concatenated SQL paths.
20
+ */
21
+ export declare class Cache {
22
+ private db;
23
+ constructor(config: CacheConfig);
24
+ private init;
25
+ upsertObject(art: Artwork): void;
26
+ getObject(id: string): Artwork | null;
27
+ putQuery(cacheKey: string, ids: string[]): void;
28
+ getQuery(cacheKey: string): string[] | null;
29
+ pruneExpired(): {
30
+ objects: number;
31
+ queries: number;
32
+ };
33
+ private isExpired;
34
+ close(): void;
35
+ }
package/dist/db.js ADDED
@@ -0,0 +1,182 @@
1
+ import Database from 'better-sqlite3';
2
+ import { chmodSync, mkdirSync, existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ const OBJECT_TTL_DAYS = 90;
5
+ const QUERY_TTL_DAYS = 14;
6
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
7
+ /**
8
+ * SQLite-backed cache for normalized artworks and search-result IDs.
9
+ *
10
+ * TTL contract:
11
+ * - `objects` rows live for {@link OBJECT_TTL_DAYS} days. Artworks don't
12
+ * change once accessioned, so the long TTL keeps cold-cache HTTP cost low.
13
+ * - `query_cache` rows live for {@link QUERY_TTL_DAYS} days. Museums add
14
+ * new open-access objects regularly, so queries refresh on a shorter cycle.
15
+ *
16
+ * Stale rows are pruned on construction (cheap bulk DELETE) so a long-lived
17
+ * client doesn't accumulate dead rows indefinitely. Reads also treat expired
18
+ * rows as misses; the next write overwrites the row via ON CONFLICT.
19
+ *
20
+ * The DB file is created with mode 0o600. Parameterized statements only —
21
+ * no string-concatenated SQL paths.
22
+ */
23
+ export class Cache {
24
+ db;
25
+ constructor(config) {
26
+ if (!existsSync(dirname(config.path))) {
27
+ mkdirSync(dirname(config.path), { recursive: true, mode: 0o700 });
28
+ }
29
+ const fileExisted = existsSync(config.path);
30
+ this.db = new Database(config.path);
31
+ if (!fileExisted) {
32
+ // Tighten file mode immediately on creation so the cache (which can
33
+ // include rights metadata, snapshots of museum responses, etc.) isn't
34
+ // group/world-readable under loose umasks.
35
+ try {
36
+ chmodSync(config.path, 0o600);
37
+ }
38
+ catch {
39
+ // Best-effort; non-fatal on filesystems that don't support chmod.
40
+ }
41
+ }
42
+ this.db.pragma('journal_mode = WAL');
43
+ this.init();
44
+ this.pruneExpired();
45
+ }
46
+ init() {
47
+ this.db.exec(`
48
+ CREATE TABLE IF NOT EXISTS objects (
49
+ id TEXT PRIMARY KEY,
50
+ museum_code TEXT NOT NULL,
51
+ title TEXT NOT NULL,
52
+ artist_name TEXT,
53
+ attribution_type TEXT NOT NULL,
54
+ display_date TEXT,
55
+ year_start INTEGER,
56
+ year_end INTEGER,
57
+ medium TEXT,
58
+ region TEXT,
59
+ period TEXT,
60
+ license_type TEXT NOT NULL,
61
+ full_record TEXT NOT NULL,
62
+ cached_at TEXT NOT NULL
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_objects_year_start ON objects(year_start);
66
+ CREATE INDEX IF NOT EXISTS idx_objects_year_end ON objects(year_end);
67
+ CREATE INDEX IF NOT EXISTS idx_objects_region ON objects(region);
68
+ CREATE INDEX IF NOT EXISTS idx_objects_period ON objects(period);
69
+ CREATE INDEX IF NOT EXISTS idx_objects_artist ON objects(artist_name);
70
+ CREATE INDEX IF NOT EXISTS idx_objects_museum ON objects(museum_code);
71
+
72
+ CREATE TABLE IF NOT EXISTS query_cache (
73
+ cache_key TEXT PRIMARY KEY,
74
+ ids_json TEXT NOT NULL,
75
+ cached_at TEXT NOT NULL
76
+ );
77
+
78
+ -- Forward-compat scaffold for v1.0 artist obscurity scoring across the
79
+ -- federated corpus. Not yet populated; do not depend on these columns.
80
+ CREATE TABLE IF NOT EXISTS artists (
81
+ name TEXT PRIMARY KEY,
82
+ object_count INTEGER NOT NULL DEFAULT 0,
83
+ museum_count INTEGER NOT NULL DEFAULT 0,
84
+ museums_json TEXT NOT NULL DEFAULT '[]'
85
+ );
86
+ `);
87
+ }
88
+ upsertObject(art) {
89
+ const stmt = this.db.prepare(`
90
+ INSERT INTO objects (
91
+ id, museum_code, title, artist_name, attribution_type,
92
+ display_date, year_start, year_end, medium, region, period,
93
+ license_type, full_record, cached_at
94
+ ) VALUES (
95
+ @id, @museum_code, @title, @artist_name, @attribution_type,
96
+ @display_date, @year_start, @year_end, @medium, @region, @period,
97
+ @license_type, @full_record, @cached_at
98
+ )
99
+ ON CONFLICT(id) DO UPDATE SET
100
+ title = excluded.title,
101
+ artist_name = excluded.artist_name,
102
+ attribution_type = excluded.attribution_type,
103
+ display_date = excluded.display_date,
104
+ year_start = excluded.year_start,
105
+ year_end = excluded.year_end,
106
+ medium = excluded.medium,
107
+ region = excluded.region,
108
+ period = excluded.period,
109
+ license_type = excluded.license_type,
110
+ full_record = excluded.full_record,
111
+ cached_at = excluded.cached_at
112
+ `);
113
+ stmt.run({
114
+ id: art.id,
115
+ museum_code: art.museum.code,
116
+ title: art.title,
117
+ artist_name: art.artist.name,
118
+ attribution_type: art.artist.attributionType,
119
+ display_date: art.displayDate,
120
+ year_start: art.yearStart,
121
+ year_end: art.yearEnd,
122
+ medium: art.medium,
123
+ region: art.region,
124
+ period: art.period,
125
+ license_type: art.license.type,
126
+ full_record: JSON.stringify(art),
127
+ cached_at: new Date().toISOString(),
128
+ });
129
+ }
130
+ getObject(id) {
131
+ const row = this.db.prepare(`SELECT full_record, cached_at FROM objects WHERE id = ?`).get(id);
132
+ if (!row)
133
+ return null;
134
+ if (this.isExpired(row.cached_at, OBJECT_TTL_DAYS))
135
+ return null;
136
+ try {
137
+ return JSON.parse(row.full_record);
138
+ }
139
+ catch {
140
+ // A malformed row (manual DB tinkering, OS-level corruption, schema
141
+ // drift from a prior version) shouldn't poison the request — treat it
142
+ // as a cache miss; the next write will overwrite the row.
143
+ return null;
144
+ }
145
+ }
146
+ putQuery(cacheKey, ids) {
147
+ this.db
148
+ .prepare(`INSERT INTO query_cache (cache_key, ids_json, cached_at)
149
+ VALUES (?, ?, ?)
150
+ ON CONFLICT(cache_key) DO UPDATE SET ids_json = excluded.ids_json, cached_at = excluded.cached_at`)
151
+ .run(cacheKey, JSON.stringify(ids), new Date().toISOString());
152
+ }
153
+ getQuery(cacheKey) {
154
+ const row = this.db.prepare(`SELECT ids_json, cached_at FROM query_cache WHERE cache_key = ?`).get(cacheKey);
155
+ if (!row)
156
+ return null;
157
+ if (this.isExpired(row.cached_at, QUERY_TTL_DAYS))
158
+ return null;
159
+ try {
160
+ return JSON.parse(row.ids_json);
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ pruneExpired() {
167
+ const objectsCutoff = new Date(Date.now() - OBJECT_TTL_DAYS * MS_PER_DAY).toISOString();
168
+ const queriesCutoff = new Date(Date.now() - QUERY_TTL_DAYS * MS_PER_DAY).toISOString();
169
+ const objects = this.db.prepare(`DELETE FROM objects WHERE cached_at < ?`).run(objectsCutoff);
170
+ const queries = this.db.prepare(`DELETE FROM query_cache WHERE cached_at < ?`).run(queriesCutoff);
171
+ return { objects: objects.changes, queries: queries.changes };
172
+ }
173
+ isExpired(isoTimestamp, ttlDays) {
174
+ const cachedAt = new Date(isoTimestamp).getTime();
175
+ const expiresAt = cachedAt + ttlDays * MS_PER_DAY;
176
+ return Date.now() > expiresAt;
177
+ }
178
+ close() {
179
+ this.db.close();
180
+ }
181
+ }
182
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Fetcher } from './types.js';
2
+ export declare const metFetcher: Fetcher;
@@ -0,0 +1,132 @@
1
+ import { parseDisplayDate } from '../dateParser.js';
2
+ import { validateMetLicense } from '../licenseGate.js';
3
+ import { cleanArtistName, detectAttributionType, normalizeRegion } from '../mappings.js';
4
+ const MET_API = 'https://collectionapi.metmuseum.org/public/collection/v1';
5
+ function asString(v) {
6
+ return typeof v === 'string' ? v : '';
7
+ }
8
+ function asOptionalString(v) {
9
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
10
+ }
11
+ export const metFetcher = {
12
+ code: 'met',
13
+ name: 'The Metropolitan Museum of Art',
14
+ async search(query, limit, options = {}) {
15
+ const url = new URL(`${MET_API}/search`);
16
+ url.searchParams.set('q', query);
17
+ if (options.hasImage !== false) {
18
+ url.searchParams.set('hasImages', 'true');
19
+ }
20
+ url.searchParams.set('isPublicDomain', 'true');
21
+ const res = await fetch(url);
22
+ if (!res.ok)
23
+ throw new Error(`Met search failed: ${res.status}`);
24
+ const json = (await res.json());
25
+ const ids = json.objectIDs ?? [];
26
+ return ids.slice(0, limit).map((n) => `met:${n}`);
27
+ },
28
+ async getRaw(id) {
29
+ const numeric = id.replace(/^met:/, '');
30
+ const res = await fetch(`${MET_API}/objects/${numeric}`);
31
+ if (!res.ok)
32
+ throw new Error(`Met get failed for ${id}: ${res.status}`);
33
+ return res.json();
34
+ },
35
+ normalize(raw) {
36
+ if (!raw || typeof raw !== 'object') {
37
+ return {
38
+ status: 'rejected',
39
+ rejection: {
40
+ id: 'met:unknown',
41
+ museumCode: 'met',
42
+ reason: 'response not an object',
43
+ rawSnapshot: raw,
44
+ },
45
+ };
46
+ }
47
+ const r = raw;
48
+ const objectID = r.objectID;
49
+ // Met IDs are always positive integers. Anything else flunks ID_REGEX
50
+ // downstream; emit a placeholder so the rejection still carries a stable
51
+ // (and obviously bogus) id for logs.
52
+ const validId = typeof objectID === 'number' && Number.isInteger(objectID) && objectID > 0;
53
+ const id = validId ? `met:${objectID}` : 'met:unknown';
54
+ const decision = validateMetLicense(raw);
55
+ if (!decision.accepted || !decision.license) {
56
+ return {
57
+ status: 'rejected',
58
+ rejection: {
59
+ id,
60
+ museumCode: 'met',
61
+ reason: decision.reason,
62
+ rawSnapshot: raw,
63
+ },
64
+ };
65
+ }
66
+ if (!validId) {
67
+ return {
68
+ status: 'rejected',
69
+ rejection: {
70
+ id,
71
+ museumCode: 'met',
72
+ reason: 'met: missing or non-integer objectID',
73
+ rawSnapshot: raw,
74
+ },
75
+ };
76
+ }
77
+ const displayDate = asString(r.objectDate);
78
+ const dateRange = parseDisplayDate(displayDate);
79
+ const artistRaw = asString(r.artistDisplayName);
80
+ const attributionType = detectAttributionType(artistRaw);
81
+ const cleanName = cleanArtistName(artistRaw);
82
+ const cultureOrCountry = asString(r.culture) || asString(r.country) || asString(r.classification);
83
+ const region = normalizeRegion(cultureOrCountry);
84
+ const periodRaw = (asString(r.period) || asString(r.dynasty)).toLowerCase().trim();
85
+ const period = periodRaw.replace(/\s*\([^)]*\)\s*$/, '').trim() || null;
86
+ const fullImage = asString(r.primaryImage) || asString(r.primaryImageSmall);
87
+ const thumbnail = asOptionalString(r.primaryImageSmall);
88
+ const beginDate = asString(r.artistBeginDate);
89
+ const endDate = asString(r.artistEndDate);
90
+ const lifespan = beginDate || endDate ? `${beginDate}–${endDate}`.replace(/^–|–$/g, '') : undefined;
91
+ const artwork = {
92
+ id,
93
+ museum: {
94
+ code: 'met',
95
+ name: 'The Metropolitan Museum of Art',
96
+ url: 'https://www.metmuseum.org',
97
+ },
98
+ title: (asString(r.title) || '(Untitled)').trim(),
99
+ artist: {
100
+ name: cleanName || 'Unknown',
101
+ nationality: asOptionalString(r.artistNationality),
102
+ lifespan,
103
+ attributionType,
104
+ },
105
+ displayDate,
106
+ yearStart: dateRange.yearStart,
107
+ yearEnd: dateRange.yearEnd,
108
+ medium: asString(r.medium),
109
+ region,
110
+ period,
111
+ imageUrls: {
112
+ full: fullImage,
113
+ thumbnail,
114
+ },
115
+ imageOpenAccess: decision.imageOpenAccess,
116
+ metadataOpenAccess: decision.metadataOpenAccess,
117
+ license: decision.license,
118
+ source: {
119
+ apiUrl: `${MET_API}/objects/${objectID}`,
120
+ pageUrl: asString(r.objectURL) || `https://www.metmuseum.org/art/collection/search/${objectID}`,
121
+ },
122
+ description: asOptionalString(r.objectName),
123
+ rawTags: Array.isArray(r.tags)
124
+ ? r.tags
125
+ .map((t) => (t && typeof t === 'object' ? t.term : undefined))
126
+ .filter((s) => typeof s === 'string')
127
+ : undefined,
128
+ };
129
+ return { status: 'accepted', artwork };
130
+ },
131
+ };
132
+ //# sourceMappingURL=met.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"met.js","sourceRoot":"","sources":["../../src/fetchers/met.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ValidationResult } from '../types.js';
2
+ export interface SearchOptions {
3
+ hasImage?: boolean;
4
+ }
5
+ export interface Fetcher {
6
+ code: string;
7
+ name: string;
8
+ search(query: string, limit: number, options?: SearchOptions): Promise<string[]>;
9
+ getRaw(id: string): Promise<unknown>;
10
+ normalize(raw: unknown): ValidationResult;
11
+ }