feat(garage): add real motorcycle image resolver
This commit is contained in:
parent
920564505f
commit
8f1ed177b0
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue