From fecc2237b8c8c31157667efa5c3dc4d6c6535657 Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 8 Jan 2026 12:22:29 +0000 Subject: [PATCH] Add aircraft photos from Planespotters.net - Backend route to proxy photo requests from Planespotters API - Frontend displays photo in Selected Target panel when available - Photos are cached to avoid repeated API calls - Clicking photo links to full image on Planespotters.net Co-Authored-By: Claude Opus 4.5 --- routes/adsb.py | 36 +++++++++++++++++++++++ templates/adsb_dashboard.html | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/routes/adsb.py b/routes/adsb.py index f38ac59..6525f98 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -511,3 +511,39 @@ def aircraft_db_delete(): """Delete aircraft database.""" result = aircraft_db.delete_database() return jsonify(result) + + +@adsb_bp.route('/aircraft-photo/') +def aircraft_photo(registration: str): + """Fetch aircraft photo from Planespotters.net API.""" + import requests + + # Validate registration format (alphanumeric with dashes) + if not registration or not all(c.isalnum() or c == '-' for c in registration): + return jsonify({'error': 'Invalid registration'}), 400 + + try: + # Planespotters.net public API + url = f'https://api.planespotters.net/pub/photos/reg/{registration}' + resp = requests.get(url, timeout=5, headers={ + 'User-Agent': 'INTERCEPT-ADS-B/1.0' + }) + + if resp.status_code == 200: + data = resp.json() + if data.get('photos') and len(data['photos']) > 0: + photo = data['photos'][0] + return jsonify({ + 'success': True, + 'thumbnail': photo.get('thumbnail_large', {}).get('src'), + 'link': photo.get('link'), + 'photographer': photo.get('photographer') + }) + + return jsonify({'success': False, 'error': 'No photo found'}) + + except requests.Timeout: + return jsonify({'success': False, 'error': 'Request timeout'}), 504 + except Exception as e: + logger.debug(f"Error fetching aircraft photo: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index c54f18f..fc1abb3 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -1591,6 +1591,12 @@ sudo make install `
${typeDesc || typeCode}${registration ? ' • ' + registration : ''}
` : ''; container.innerHTML = ` +
${callsign}
${typeInfo} ${badge} @@ -1632,6 +1638,55 @@ sudo make install
${ac.lat ? calculateDistanceNm(observerLocation.lat, observerLocation.lon, ac.lat, ac.lon).toFixed(1) + ' nm' : 'N/A'}
`; + + // Fetch aircraft photo if registration is available + if (registration) { + fetchAircraftPhoto(registration); + } + } + + // Cache for aircraft photos to avoid repeated API calls + const photoCache = {}; + + async function fetchAircraftPhoto(registration) { + const container = document.getElementById('aircraftPhotoContainer'); + const img = document.getElementById('aircraftPhoto'); + const link = document.getElementById('aircraftPhotoLink'); + const credit = document.getElementById('aircraftPhotoCredit'); + + if (!container || !img) return; + + // Check cache first + if (photoCache[registration]) { + const cached = photoCache[registration]; + if (cached.thumbnail) { + img.src = cached.thumbnail; + link.href = cached.link || '#'; + credit.textContent = cached.photographer ? `Photo: ${cached.photographer}` : ''; + container.style.display = 'block'; + } + return; + } + + try { + const response = await fetch(`/adsb/aircraft-photo/${encodeURIComponent(registration)}`); + const data = await response.json(); + + // Cache the result + photoCache[registration] = data; + + if (data.success && data.thumbnail) { + img.src = data.thumbnail; + link.href = data.link || '#'; + credit.textContent = data.photographer ? `Photo: ${data.photographer}` : ''; + container.style.display = 'block'; + } else { + container.style.display = 'none'; + } + } catch (err) { + console.debug('Failed to fetch aircraft photo:', err); + container.style.display = 'none'; + } } function cleanupOldAircraft() {