feat(garage): add real motorcycle image resolver

This commit is contained in:
bponders12-cell 2026-09-01 12:10:22 -04:00
parent 920564505f
commit 8f1ed177b0
1 changed files with 155 additions and 0 deletions

View File

@ -0,0 +1,155 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
class MotorcycleImage {
final String imageUrl;
final String sourceUrl;
final String? credit;
final String? license;
const MotorcycleImage({
required this.imageUrl,
required this.sourceUrl,
this.credit,
this.license,
});
}
/// Resolves a real motorcycle photograph for a make/model/year selection.
///
/// Known exact matches are preferred first. Other motorcycles are resolved
/// from Wikimedia Commons using its public API, with licensing metadata kept
/// alongside the image for attribution in the UI.
class MotorcycleImageService {
MotorcycleImageService._();
static final MotorcycleImageService instance = MotorcycleImageService._();
final Map<String, MotorcycleImage?> _cache = {};
static const _exactMatches = <String, MotorcycleImage>{
'2001|yamaha|yzf-r6': MotorcycleImage(
imageUrl:
'https://upload.wikimedia.org/wikipedia/commons/e/e0/2001_Yamaha_YZFR600.jpg',
sourceUrl:
'https://commons.wikimedia.org/wiki/File:2001_Yamaha_YZFR600.jpg',
credit: 'James McCauley / Wikimedia Commons',
license: 'CC BY 2.0',
),
};
Future<MotorcycleImage?> resolve({
required int year,
required String make,
required String model,
}) async {
final key = _key(year, make, model);
if (_cache.containsKey(key)) return _cache[key];
final exact = _exactMatches[key];
if (exact != null) {
_cache[key] = exact;
return exact;
}
try {
final query = '$year $make $model motorcycle';
final uri = Uri.https('commons.wikimedia.org', '/w/api.php', {
'action': 'query',
'format': 'json',
'origin': '*',
'generator': 'search',
'gsrnamespace': '6',
'gsrsearch': query,
'gsrlimit': '8',
'prop': 'imageinfo',
'iiprop': 'url|extmetadata',
'iiurlwidth': '900',
'iiextmetadatafilter': 'Artist|Credit|LicenseShortName|LicenseUrl',
});
final response = await http.get(uri, headers: const {
'Accept': 'application/json',
'User-Agent': 'MotorSignal/1.0 (motorcycle garage image lookup)',
}).timeout(const Duration(seconds: 8));
if (response.statusCode != 200) {
_cache[key] = null;
return null;
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final pages = (json['query']?['pages'] as Map?)?.values;
if (pages == null) {
_cache[key] = null;
return null;
}
MotorcycleImage? best;
var bestScore = -1;
final normalizedModel = _normalize(model);
final normalizedMake = _normalize(make);
final normalizedYear = year.toString();
for (final rawPage in pages) {
final page = Map<String, dynamic>.from(rawPage as Map);
final title = (page['title']?.toString() ?? '').toLowerCase();
final infoList = page['imageinfo'];
if (infoList is! List || infoList.isEmpty) continue;
final info = Map<String, dynamic>.from(infoList.first as Map);
final thumb = info['thumburl']?.toString() ?? info['url']?.toString();
final source = info['descriptionurl']?.toString();
if (thumb == null || source == null) continue;
final metadata = Map<String, dynamic>.from(
(info['extmetadata'] as Map?) ?? const {},
);
final license = _metadataValue(metadata, 'LicenseShortName');
final licenseLower = (license ?? '').toLowerCase();
final allowed = licenseLower.contains('cc by') ||
licenseLower.contains('public domain') ||
licenseLower.contains('cc0');
if (!allowed) continue;
var score = 0;
if (title.contains(normalizedModel)) score += 5;
if (title.contains(normalizedMake)) score += 3;
if (title.contains(normalizedYear)) score += 4;
if (title.contains('motorcycle') || title.contains('motorbike')) score += 1;
if (score > bestScore) {
bestScore = score;
best = MotorcycleImage(
imageUrl: thumb,
sourceUrl: source,
credit: _metadataValue(metadata, 'Artist') ??
_metadataValue(metadata, 'Credit'),
license: license,
);
}
}
_cache[key] = best;
return best;
} catch (error) {
debugPrint('Motorcycle image lookup failed: $error');
_cache[key] = null;
return null;
}
}
String _key(int year, String make, String model) =>
'$year|${_normalize(make)}|${_normalize(model)}';
String _normalize(String value) => value
.toLowerCase()
.replaceAll(RegExp(r'[^a-z0-9]+'), '-')
.replaceAll(RegExp(r'^-+|-+$'), '');
String? _metadataValue(Map<String, dynamic> metadata, String key) {
final value = metadata[key];
if (value is Map) return value['value']?.toString();
return value?.toString();
}
}