Places: - worker/geocode.js: Nominatim reverse geocoder, 1 req/sec rate limit, in-memory cache keyed to 3-decimal-place coordinates - Ingest calls reverseGeocode() for any photo with GPS data; stores place_city and place_country on the photo row - DB migrations add place_city / place_country columns + index on place_city - Stats API returns cities grouped by count - buildFilter() supports ?city= parameter - FilterSidebar shows a Places section with city list + country sublabel Folder browser: - GET /api/fs/browse?path=… lists subdirectories (path-normalized to prevent traversal) - Settings panel: 📁 button opens inline directory browser below the add input — navigate with up arrow / directory clicks, Select fills the input Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 lines
1.1 KiB
JavaScript
37 lines
1.1 KiB
JavaScript
const cache = new Map();
|
|
let lastCallMs = 0;
|
|
|
|
export async function reverseGeocode(lat, lon) {
|
|
const key = `${lat.toFixed(3)},${lon.toFixed(3)}`;
|
|
if (cache.has(key)) return cache.get(key);
|
|
|
|
// Nominatim ToS: max 1 request/second
|
|
const wait = 1100 - (Date.now() - lastCallMs);
|
|
if (wait > 0) await new Promise(r => setTimeout(r, wait));
|
|
lastCallMs = Date.now();
|
|
|
|
try {
|
|
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`;
|
|
const res = await fetch(url, {
|
|
headers: { 'User-Agent': 'Luma/1.0 self-hosted-photo-manager' },
|
|
signal: AbortSignal.timeout(8000),
|
|
});
|
|
if (!res.ok) throw new Error(res.status);
|
|
const data = await res.json();
|
|
const place = {
|
|
city: data.address?.city
|
|
?? data.address?.town
|
|
?? data.address?.village
|
|
?? data.address?.county
|
|
?? null,
|
|
country: data.address?.country ?? null,
|
|
};
|
|
cache.set(key, place);
|
|
return place;
|
|
} catch (err) {
|
|
console.warn(`[geocode] ${lat.toFixed(4)},${lon.toFixed(4)}: ${err.message}`);
|
|
cache.set(key, null);
|
|
return null;
|
|
}
|
|
}
|