MotorSignal-App/website/app/api/motorcycle-image/route.ts

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 });
}