237 lines
8.6 KiB
TypeScript
237 lines
8.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
interface ResolvedMotorcycleImage {
|
|
imageUrl: string;
|
|
caption: string;
|
|
source: 'google' | 'curated' | 'wikimedia' | 'wikipedia';
|
|
attribution: string;
|
|
}
|
|
|
|
// Curated high-resolution stock photography for flagship motorcycles
|
|
const CURATED_IMAGES: Record<string, ResolvedMotorcycleImage> = {
|
|
'yamaha_yzf-r6_2001': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e0/2001_Yamaha_YZFR600.jpg/960px-2001_Yamaha_YZFR600.jpg',
|
|
caption: '2001 Yamaha YZF-R6 (5EB)',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'yamaha_yzf-r6': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/d/dd/Yamaha_YZF-R6_Cropped.jpg/960px-Yamaha_YZF-R6_Cropped.jpg',
|
|
caption: 'Yamaha YZF-R6 Supersport',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'ducati_panigale-v4-s': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a7/Ducati_Panigale_V4_S_Corse.jpg/960px-Ducati_Panigale_V4_S_Corse.jpg',
|
|
caption: 'Ducati Panigale V4 S Corse',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'ducati_panigale-v4': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a8/Ducati_Panigale_V4_-_Tuning_World_Bodensee_2018%2C_Friedrichshafen_%28OW1A0484%29.jpg/3840px-Ducati_Panigale_V4_-_Tuning_World_Bodensee_2018%2C_Friedrichshafen_%28OW1A0484%29.jpg',
|
|
caption: 'Ducati Panigale V4',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'bmw_s1000rr': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a0/2022-05_BMW_M_Motorrad_S1000RR.jpg/960px-2022-05_BMW_M_Motorrad_S1000RR.jpg',
|
|
caption: 'BMW S1000RR Superbike',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'honda_cbr600rr': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/6/64/2006HondaCBR600RR-001.jpg/960px-2006HondaCBR600RR-001.jpg',
|
|
caption: 'Honda CBR600RR',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
'kawasaki_ninja-zx-6r': {
|
|
imageUrl:
|
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/9/93/2009_Kawasaki_Ninja_ZX-6R_in_white.jpg/960px-2009_Kawasaki_Ninja_ZX-6R_in_white.jpg',
|
|
caption: 'Kawasaki Ninja ZX-6R',
|
|
source: 'curated',
|
|
attribution: 'Wikimedia Commons / CC BY',
|
|
},
|
|
};
|
|
|
|
function normalizeKey(make: string, model: string, year?: number): string {
|
|
const normMake = make.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
const normModel = model.toLowerCase().replace(/standard/g, '').replace(/[^a-z0-9]/g, '');
|
|
return year ? `${normMake}_${normModel}_${year}` : `${normMake}_${normModel}`;
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const { searchParams } = new URL(request.url);
|
|
const make = (searchParams.get('make') || '').trim();
|
|
const rawModel = (searchParams.get('model') || '').trim();
|
|
const yearParam = searchParams.get('year');
|
|
const year = yearParam ? parseInt(yearParam, 10) : undefined;
|
|
|
|
if (!make && !rawModel) {
|
|
return NextResponse.json({ error: 'Make and model are required' }, { status: 400 });
|
|
}
|
|
|
|
// Clean model name (remove "Standard", "Base", etc.)
|
|
const cleanModel = rawModel
|
|
.replace(/\bstandard\b/gi, '')
|
|
.replace(/\bbase\b/gi, '')
|
|
.trim();
|
|
|
|
// Tier 1: Check curated exact and model cache
|
|
if (year) {
|
|
const exactCurated = CURATED_IMAGES[normalizeKey(make, cleanModel, year)];
|
|
if (exactCurated) {
|
|
return NextResponse.json(exactCurated, {
|
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
|
});
|
|
}
|
|
}
|
|
|
|
const modelCurated =
|
|
CURATED_IMAGES[normalizeKey(make, cleanModel)] ||
|
|
CURATED_IMAGES[cleanModel.toLowerCase().replace(/[^a-z0-9]/g, '')];
|
|
if (modelCurated) {
|
|
return NextResponse.json(modelCurated, {
|
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
|
});
|
|
}
|
|
|
|
// Tier 2: Google Custom Search API (if configured in environment)
|
|
const googleKey = process.env.GOOGLE_SEARCH_API_KEY;
|
|
const googleCx = process.env.GOOGLE_SEARCH_ENGINE_ID;
|
|
if (googleKey && googleCx) {
|
|
try {
|
|
const q = [year, make, cleanModel, 'motorcycle stock photo'].filter(Boolean).join(' ');
|
|
const googleUrl = `https://www.googleapis.com/customsearch/v1?key=${encodeURIComponent(
|
|
googleKey
|
|
)}&cx=${encodeURIComponent(googleCx)}&q=${encodeURIComponent(q)}&searchType=image&num=3&safe=active`;
|
|
|
|
const gRes = await fetch(googleUrl, { next: { revalidate: 604800 } });
|
|
if (gRes.ok) {
|
|
const gData = await gRes.json();
|
|
const firstItem = gData.items?.[0];
|
|
if (firstItem?.link) {
|
|
const result: ResolvedMotorcycleImage = {
|
|
imageUrl: firstItem.link,
|
|
caption: firstItem.title || `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
|
source: 'google',
|
|
attribution: firstItem.displayLink || 'Google Image Search',
|
|
};
|
|
return NextResponse.json(result, {
|
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Google Custom Search API error, falling back:', err);
|
|
}
|
|
}
|
|
|
|
// Tier 3: Wikimedia Commons API Search
|
|
const searchQueries: string[] = [];
|
|
if (year && year > 1900) {
|
|
searchQueries.push(`${year} ${make} ${cleanModel}`);
|
|
}
|
|
searchQueries.push(`${make} ${cleanModel}`);
|
|
searchQueries.push(`${cleanModel}`);
|
|
|
|
for (const query of searchQueries) {
|
|
try {
|
|
const commonsUrl = `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrnamespace=6&gsrsearch=${encodeURIComponent(
|
|
query
|
|
)}&gsrlimit=6&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=960`;
|
|
|
|
const cRes = await fetch(commonsUrl, {
|
|
headers: {
|
|
'User-Agent': 'MotorSignal/1.0 (https://motorsignal.app; contact@motorsignal.app)',
|
|
},
|
|
next: { revalidate: 604800 },
|
|
});
|
|
|
|
if (cRes.ok) {
|
|
const cData = await cRes.json();
|
|
const pages = cData.query?.pages;
|
|
if (pages) {
|
|
for (const rawPage of Object.values(pages) as any[]) {
|
|
const title = (rawPage.title || '').toLowerCase();
|
|
if (
|
|
title.includes('logo') ||
|
|
title.includes('icon') ||
|
|
title.includes('diagram') ||
|
|
title.includes('flag') ||
|
|
title.includes('badge')
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const info = rawPage.imageinfo?.[0];
|
|
const imgUrl = info?.thumburl || info?.url;
|
|
if (imgUrl && /\.(jpe?g|png|webp)/i.test(imgUrl)) {
|
|
const result: ResolvedMotorcycleImage = {
|
|
imageUrl: imgUrl,
|
|
caption: `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
|
source: 'wikimedia',
|
|
attribution: 'Wikimedia Commons / CC',
|
|
};
|
|
return NextResponse.json(result, {
|
|
headers: {
|
|
'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Wikimedia query error:', err);
|
|
}
|
|
}
|
|
|
|
// Tier 4: Wikipedia Lead Summary Image
|
|
const wikiTitles = [
|
|
`${make} ${cleanModel}`,
|
|
cleanModel,
|
|
`${make}_${cleanModel}`.replace(/\s+/g, '_'),
|
|
];
|
|
|
|
for (const title of wikiTitles) {
|
|
try {
|
|
const wikiUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
|
|
const wRes = await fetch(wikiUrl, {
|
|
headers: {
|
|
'User-Agent': 'MotorSignal/1.0 (https://motorsignal.app; contact@motorsignal.app)',
|
|
},
|
|
next: { revalidate: 604800 },
|
|
});
|
|
|
|
if (wRes.ok) {
|
|
const wData = await wRes.json();
|
|
const img = wData.originalimage?.source || wData.thumbnail?.source;
|
|
if (img && !img.includes('disambig') && !img.includes('Question_book')) {
|
|
const result: ResolvedMotorcycleImage = {
|
|
imageUrl: img,
|
|
caption: wData.title || `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
|
source: 'wikipedia',
|
|
attribution: 'Wikipedia Lead Reference',
|
|
};
|
|
return NextResponse.json(result, {
|
|
headers: {
|
|
'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Wikipedia query error:', err);
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ error: 'No stock photo found for this motorcycle' }, { status: 404 });
|
|
}
|