PageSourceSearch

https://www.hetphoenixproject.nl/kunst/api-europeana.js

js hetphoenixproject.nl collected 2026-09-25 03:38:22 UTC 13,317 bytes, 298 lines download raw bytes

1// Europeana API Module
2// REST API for 50+ million objects from 3000+ European cultural institutions
3// Using the official Search API - no CORS issues!
4
5const EuropeanaAPI = {
6    config: {
7        name: 'Europeana',
8        apiUrl: 'https://api.europeana.eu/record/v2/search.json',
9        // API key - register for free at: https://pro.europeana.eu/pages/get-api
10        apiKey: 'arkyaphormir',
11        type: 'rest',
12        enabled: true // Now enabled with REST API!
13    },
14
15    cache: new Map(),
16    
17    // Store last search params for pagination
18    lastSearch: {
19        query: '',
20        imgOnly: false,
21        objectType: '',
22        cursor: null,
23        totalResults: 0
24    },
25
26    async search(searchTerm = '', imgOnly = false, objectType = '', cursor = null) {
27        if (!searchTerm) {
28            // Browse mode - get recent Dutch artworks
29            searchTerm = '*';
30        }
31
32        // Check cache
33        const cacheKey = `eu_${searchTerm.toLowerCase()}_img${imgOnly}_type${objectType}`;
34        if (this.cache.has(cacheKey)) {
35            return this.cache.get(cacheKey);
36        }
37
38        try {
39            // Map Dutch object types to multiple English/international variations
40            // Europeana uses free-text dc:type fields, so we need to search for multiple terms
41            const typeMapping = {
42                'schilderij': ['painting', 'schilderij', 'Gemälde', 'peinture', 'oil painting', 'canvas'],
43                'schilderijen': ['painting', 'schilderij', 'Gemälde', 'peinture', 'oil painting', 'canvas'],
44                'tekening': ['drawing', 'tekening', 'dessin', 'Zeichnung', 'sketch'],
45                'tekeningen': ['drawing', 'tekening', 'dessin', 'Zeichnung', 'sketch'],
46                'prent': ['print', 'prent', 'estampe', 'Druck', 'engraving', 'etching', 'ets', 'gravure'],
47                'prenten': ['print', 'prent', 'estampe', 'Druck', 'engraving', 'etching', 'ets', 'gravure'],
48                'ets': ['etching', 'ets', 'eau-forte', 'Radierung', 'print'],
49                'etsen': ['etching', 'ets', 'eau-forte', 'Radierung', 'print'],
50                'gravure': ['engraving', 'gravure', 'print', 'prent'],
51                'foto': ['photograph', 'photo', 'foto', 'photographie', 'photography'],
52                'foto\'s': ['photograph', 'photo', 'foto', 'photographie', 'photography'],
53                'boek': ['book', 'boek', 'livre', 'Buch'],
54                'boeken': ['book', 'boek', 'livre', 'Buch'],
55                'sculptuur': ['sculpture', 'sculptuur', 'beeldhouwwerk', 'statue'],
56                'beeldhouwwerk': ['sculpture', 'sculptuur', 'beeldhouwwerk', 'statue'],
57                'aquarel': ['watercolor', 'watercolour', 'aquarel', 'aquarelle'],
58                'aquarellen': ['watercolor', 'watercolour', 'aquarel', 'aquarelle'],
59                'litho': ['lithograph', 'lithography', 'litho', 'lithografie'],
60                'lithografie': ['lithograph', 'lithography', 'litho', 'lithografie'],
61                'houtsnede': ['woodcut', 'houtsnede', 'xylographie', 'Holzschnitt'],
62                'poster': ['poster', 'affiche', 'Plakat'],
63                'affiches': ['poster', 'affiche', 'Plakat']
64            };
65            
66            // Build query - search in title and creator
67            let query = searchTerm === '*' ? '*' : `(title:${searchTerm} OR proxy_dc_creator:${searchTerm})`;
68            
69            // Build query filters array
70            let qfParams = [
71                'COUNTRY:netherlands',
72                'TYPE:IMAGE'
73            ];
74            
75            // Add object type filter if provided - this is the primary filter
76            if (objectType) {
77                const lowerType = objectType.toLowerCase();
78                const variants = typeMapping[lowerType] || [objectType];
79                
80                // Use qf (query filter) for better filtering on dc:type
81                // This is more strict than searching in the main query
82                const typeQuery = variants.map(v => `proxy_dc_type:"${v}"`).join(' OR ');
83                query += ` AND (${typeQuery})`;
84            } else {
85                // Only add art/cultural heritage filter when NO object type is specified
86                // This prevents the filter from interfering with specific type searches
87                query += ' AND (proxy_dc_subject:art OR proxy_dc_subject:kunst OR proxy_dc_type:painting OR proxy_dc_type:drawing OR proxy_dc_type:print OR proxy_dc_type:photograph)';
88            }
89            
90            // Build URL with qf parameters
91            // Note: media parameter must be string 'true' not boolean
92            const qfString = qfParams.map(qf => `&qf=${encodeURIComponent(qf)}`).join('');
93            const mediaParam = imgOnly ? '&media=true' : '';
94            const cursorParam = cursor ? `&cursor=${encodeURIComponent(cursor)}` : '';
95            const url = `${this.config.apiUrl}?wskey=${this.config.apiKey}&query=${encodeURIComponent(query)}${qfString}&rows=50&profile=rich${mediaParam}&thumbnail=true${cursorParam}`;
96            const response = await fetch(url);
97            if (!response.ok) {
98                const errorText = await response.text();
99                return [];
100            }
101
102            const data = await response.json();
103            if (!data.success) {
104                return [];
105            }
106
107            const items = data.items || [];
108            // Convert Europeana results to our format
109            const artworks = items.map(item => {
110                // Extract image URL - prioritize edmIsShownBy, then edmPreview
111                let imageUrl = item.edmIsShownBy?.[0] || item.edmPreview?.[0] || '';
112                
113                // Get creator - can be array or string
114                const creator = Array.isArray(item.dcCreator) 
115                    ? item.dcCreator[0] 
116                    : item.dcCreator || 'Onbekende kunstenaar';
117                
118                // Get title - can be array or string
119                const title = Array.isArray(item.title) 
120                    ? item.title[0] 
121                    : item.title || 'Onbekend werk';
122                
123                // Get date
124                const date = Array.isArray(item.year) 
125                    ? item.year[0] 
126                    : item.year || '';
127                
128                // Get object type from dcType (e.g., "painting", "drawing"), not media type
129                const dcType = Array.isArray(item.dcType) 
130                    ? item.dcType[0] 
131                    : item.dcType || '';
132
133                return {
134                    id: item.id || '',
135                    title: title,
136                    creator: creator,
137                    principalOrFirstMaker: creator,
138                    creationDate: date,
139                    description: Array.isArray(item.dcDescription) ? item.dcDescription[0] : item.dcDescription || '',
140                    webImage: imageUrl ? { url: imageUrl } : null,
141                    image: imageUrl ? { url: imageUrl } : null,
142                    objectNumber: item.id || '',
143                    type: dcType || 'Kunstwerk',
144                    technique: '',
145                    material: '',
146                    dimensions: '',
147                    externalUrl: item.guid || '',
148                    dataProvider: Array.isArray(item.dataProvider) ? item.dataProvider[0] : item.dataProvider || '',
149                    rights: Array.isArray(item.rights) ? item.rights[0] : item.rights || '',
150                    source: 'europeana',
151                    sourceLabel: 'Europeana',
152                    originalData: item
153                };
154            }).filter(artwork => artwork.id && (!imgOnly || artwork.webImage)); // Filter items without images if imgOnly
155            
156            // Get total results count from Europeana response
157            const totalResults = data.totalResults || artworks.length;
158            
159            // Store search params for pagination
160            this.lastSearch = {
161                query: searchTerm,
162                imgOnly,
163                objectType,
164                cursor: data.nextCursor || null,
165                totalResults
166            };
167            
168            // Cache results (only first page)
169            if (!cursor) {
170                const result = { items: artworks, total: totalResults, nextCursor: data.nextCursor, hasMore: !!data.nextCursor };
171                this.cache.set(cacheKey, result);
172            }
173            
174            return { 
175                items: artworks, 
176                total: totalResults, 
177                nextCursor: data.nextCursor || null,
178                hasMore: !!data.nextCursor && artworks.length > 0
179            };
180
181        } catch (error) {
182            return { items: [], total: 0, hasMore: false };
183        }
184    },
185    
186    /**
187     * Load more results using cursor-based pagination
188     */
189    async loadMore() {
190        if (!this.lastSearch.cursor) {
191            return { items: [], total: 0, hasMore: false };
192        }
193        
194        return this.search(
195            this.lastSearch.query,
196            this.lastSearch.imgOnly,
197            this.lastSearch.objectType,
198            this.lastSearch.cursor
199        );
200    },
201
202    /**
203     * Search specifically by creator/artist name (not in title)
204     */
205    async searchByCreator(creatorName, imgOnly = true) {
206        if (!creatorName) {
207            return { items: [], total: 0 };
208        }
209
210        const cacheKey = `eu_creator_${creatorName.toLowerCase()}_img${imgOnly}`;
211        if (this.cache.has(cacheKey)) {
212            return this.cache.get(cacheKey);
213        }
214
215        try {
216            // Search ONLY in creator field, not title
217            let query = `proxy_dc_creator:"${creatorName}"`;
218            
219            // Build query filters
220            let qfParams = [
221                'TYPE:IMAGE'
222            ];
223            
224            if (imgOnly) {
225                qfParams.push('MEDIA:true');
226            }
227
228            const params = new URLSearchParams({
229                wskey: this.config.apiKey,
230                query: query,
231                rows: '100',
232                profile: 'standard',
233                sort: 'score+desc'
234            });
235            
236            qfParams.forEach(qf => params.append('qf', qf));
237            
238            const url = `${this.config.apiUrl}?${params}`;
239            const response = await fetch(url);
240            
241            if (!response.ok) {
242                throw new Error(`Europeana API error: ${response.status}`);
243            }
244
245            const data = await response.json();
246            
247            if (!data.items || data.items.length === 0) {
248                return { items: [], total: 0 };
249            }
250
251            const artworks = data.items.map(item => {
252                const imageUrl = item.edmPreview ? (Array.isArray(item.edmPreview) ? item.edmPreview[0] : item.edmPreview) : null;
253                const title = Array.isArray(item.title) ? item.title[0] : item.title || 'Zonder titel';
254                const creator = Array.isArray(item.dcCreator) ? item.dcCreator[0] : item.dcCreator || 'Onbekend';
255                const date = Array.isArray(item.year) ? item.year[0] : item.year || '';
256                
257                // Get object type from dcType (e.g., "painting", "drawing"), not media type
258                const dcType = Array.isArray(item.dcType) ? item.dcType[0] : item.dcType || '';
259
260                return {
261                    id: item.id || '',
262                    title: title,
263                    creator: creator,
264                    principalOrFirstMaker: creator,
265                    creationDate: date,
266                    description: Array.isArray(item.dcDescription) ? item.dcDescription[0] : item.dcDescription || '',
267                    webImage: imageUrl ? { url: imageUrl } : null,
268                    image: imageUrl ? { url: imageUrl } : null,
269                    objectNumber: item.id || '',
270                    type: dcType || 'Kunstwerk',
271                    technique: '',
272                    material: '',
273                    dimensions: '',
274                    externalUrl: item.guid || '',
275                    dataProvider: Array.isArray(item.dataProvider) ? item.dataProvider[0] : item.dataProvider || '',
276                    rights: Array.isArray(item.rights) ? item.rights[0] : item.rights || '',
277                    source: 'europeana',
278                    sourceLabel: 'Europeana',
279                    originalData: item
280                };
281            }).filter(artwork => artwork.id && (!imgOnly || artwork.webImage));
282
283            const result = { items: artworks, total: data.totalResults || artworks.length };
284            this.cache.set(cacheKey, result);
285            return result;
286
287        } catch (error) {
288            console.error('Europeana creator search error:', error);
289            return { items: [], total: 0 };
290        }
291    }
292};
293
294// Export for use in main script
295if (typeof window !== 'undefined') {
296    window.EuropeanaAPI = EuropeanaAPI;
297}
298

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.