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.