mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee31c78db8 | |||
| edf14e2ebc | |||
| 2aad8f465c |
@@ -477,7 +477,11 @@ export const MapView = memo(function MapView({
|
|||||||
cleanups.push(onThumbReady(cacheKey, thumb => setThumb(cacheKey, thumb)))
|
cleanups.push(onThumbReady(cacheKey, thumb => setThumb(cacheKey, thumb)))
|
||||||
|
|
||||||
if (!cached && !isLoading(cacheKey)) {
|
if (!cached && !isLoading(cacheKey)) {
|
||||||
const photoId = place.image_url || place.google_place_id || place.osm_id
|
const photoId =
|
||||||
|
(place.image_url?.startsWith('/api/maps/place-photo/') ? place.image_url : null)
|
||||||
|
|| place.google_place_id
|
||||||
|
|| place.osm_id
|
||||||
|
|| place.image_url
|
||||||
if (photoId || (place.lat && place.lng)) {
|
if (photoId || (place.lat && place.lng)) {
|
||||||
fetchPhoto(cacheKey, photoId || `coords:${place.lat}:${place.lng}`, place.lat, place.lng, place.name)
|
fetchPhoto(cacheKey, photoId || `coords:${place.lat}:${place.lng}`, place.lat, place.lng, place.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,7 +366,11 @@ export function MapViewGL({
|
|||||||
}
|
}
|
||||||
cleanups.push(onThumbReady(cacheKey, thumb => setThumb(cacheKey, thumb)))
|
cleanups.push(onThumbReady(cacheKey, thumb => setThumb(cacheKey, thumb)))
|
||||||
if (!cached && !isLoading(cacheKey)) {
|
if (!cached && !isLoading(cacheKey)) {
|
||||||
const photoId = place.image_url || place.google_place_id || place.osm_id
|
const photoId =
|
||||||
|
(place.image_url?.startsWith('/api/maps/place-photo/') ? place.image_url : null)
|
||||||
|
|| place.google_place_id
|
||||||
|
|| place.osm_id
|
||||||
|
|| place.image_url
|
||||||
if (photoId || (place.lat && place.lng)) {
|
if (photoId || (place.lat && place.lng)) {
|
||||||
fetchPhoto(cacheKey, photoId || `coords:${place.lat}:${place.lng}`, place.lat, place.lng, place.name)
|
fetchPhoto(cacheKey, photoId || `coords:${place.lat}:${place.lng}`, place.lat, place.lng, place.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1906,6 +1906,46 @@ function runMigrations(db: Database.Database): void {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_day_accommodations_end_day_id ON day_accommodations(end_day_id);
|
CREATE INDEX IF NOT EXISTS idx_day_accommodations_end_day_id ON day_accommodations(end_day_id);
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
|
// Migration: null out proxy image_url entries that have no backing disk cache.
|
||||||
|
// Migrations 107 and the migration below wrote /api/maps/place-photo/<id>/bytes
|
||||||
|
// into places.image_url without actually fetching/caching the photo bytes. The
|
||||||
|
// photoService short-circuits on that prefix and hits /bytes directly → 404.
|
||||||
|
// Rows with a confirmed disk cache entry in google_place_photo_meta are left alone;
|
||||||
|
// only stale proxy URLs (never actually fetched) are cleared so the normal
|
||||||
|
// fetch-and-cache flow can repopulate them.
|
||||||
|
() => {
|
||||||
|
db.exec(`
|
||||||
|
UPDATE places
|
||||||
|
SET image_url = NULL, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE image_url LIKE '/api/maps/place-photo/%/bytes'
|
||||||
|
AND google_place_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM google_place_photo_meta
|
||||||
|
WHERE place_id = places.google_place_id
|
||||||
|
AND error_at IS NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
// Migration: clear legacy Google photo URLs missed by Migration 107.
|
||||||
|
// Migration 107 matched /places/%/photos/% only; lh3.googleusercontent.com URLs use
|
||||||
|
// /place-photos/ or /places/<opaque-id> paths and were skipped. NULL those stale URLs
|
||||||
|
// so the normal fetch-and-cache flow repopulates image_url with a real proxy URL.
|
||||||
|
() => {
|
||||||
|
db.exec(`
|
||||||
|
UPDATE places
|
||||||
|
SET image_url = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE image_url IS NOT NULL
|
||||||
|
AND image_url != ''
|
||||||
|
AND image_url NOT LIKE '/api/maps/place-photo/%'
|
||||||
|
AND (
|
||||||
|
image_url LIKE 'http://%googleusercontent.com/%'
|
||||||
|
OR image_url LIKE 'https://%googleusercontent.com/%'
|
||||||
|
OR image_url LIKE 'http://%places.googleapis.com/%'
|
||||||
|
OR image_url LIKE 'https://%places.googleapis.com/%'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -648,6 +648,12 @@ export async function getPlacePhoto(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject URL-shaped placeIds — legacy DBs may store raw photo URLs in image_url
|
||||||
|
if (/^https?:\/\//i.test(placeId)) {
|
||||||
|
placePhotoCache.markError(placeId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Google Photos — fetch details to get photo name
|
// Google Photos — fetch details to get photo name
|
||||||
const detailsRes = await googleFetch(`https://places.googleapis.com/v1/places/${placeId}`, `getPlacePhoto/details(${placeId})`, {
|
const detailsRes = await googleFetch(`https://places.googleapis.com/v1/places/${placeId}`, `getPlacePhoto/details(${placeId})`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -655,13 +661,15 @@ export async function getPlacePhoto(
|
|||||||
'X-Goog-FieldMask': 'photos',
|
'X-Goog-FieldMask': 'photos',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const details = await detailsRes.json() as GooglePlaceDetails & { error?: { message?: string } };
|
const body = await detailsRes.text();
|
||||||
|
|
||||||
if (!detailsRes.ok) {
|
if (!detailsRes.ok) {
|
||||||
console.error('Google Places photo details error:', details.error?.message || detailsRes.status);
|
console.error('Google Places photo details error:', detailsRes.status, body.slice(0, 200));
|
||||||
placePhotoCache.markError(placeId);
|
placePhotoCache.markError(placeId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
let details: GooglePlaceDetails & { error?: { message?: string } };
|
||||||
|
try { details = body ? JSON.parse(body) : { photos: [] }; }
|
||||||
|
catch { placePhotoCache.markError(placeId); return null; }
|
||||||
|
|
||||||
if (!details.photos?.length) {
|
if (!details.photos?.length) {
|
||||||
placePhotoCache.markError(placeId);
|
placePhotoCache.markError(placeId);
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export function getInFlight(placeId: string): Promise<{ filePath: string; attrib
|
|||||||
|
|
||||||
export function setInFlight(placeId: string, promise: Promise<{ filePath: string; attribution: string | null } | null>): void {
|
export function setInFlight(placeId: string, promise: Promise<{ filePath: string; attribution: string | null } | null>): void {
|
||||||
inFlight.set(placeId, promise);
|
inFlight.set(placeId, promise);
|
||||||
promise.finally(() => inFlight.delete(placeId));
|
promise
|
||||||
|
.finally(() => inFlight.delete(placeId))
|
||||||
|
.catch(() => { /* awaiter logs; this .catch only prevents unhandledRejection */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serveFilePath(placeId: string): string | null {
|
export function serveFilePath(placeId: string): string | null {
|
||||||
|
|||||||
@@ -1235,7 +1235,7 @@ describe('getPlacePhoto (fetch stubbed)', () => {
|
|||||||
// First call: get place details (with photos)
|
// First call: get place details (with photos)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
text: async () => JSON.stringify({
|
||||||
photos: [{ name: 'places/ChIJABC/photos/photo1', authorAttributions: [{ displayName: 'Photographer' }] }],
|
photos: [{ name: 'places/ChIJABC/photos/photo1', authorAttributions: [{ displayName: 'Photographer' }] }],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -1258,7 +1258,7 @@ describe('getPlacePhoto (fetch stubbed)', () => {
|
|||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 403,
|
status: 403,
|
||||||
json: async () => ({ error: { message: 'Forbidden' } }),
|
text: async () => JSON.stringify({ error: { message: 'Forbidden' } }),
|
||||||
}));
|
}));
|
||||||
const { getPlacePhoto } = await import('../../../src/services/mapsService');
|
const { getPlacePhoto } = await import('../../../src/services/mapsService');
|
||||||
const errId = `ChIJErr-${Date.now()}`;
|
const errId = `ChIJErr-${Date.now()}`;
|
||||||
@@ -1269,7 +1269,7 @@ describe('getPlacePhoto (fetch stubbed)', () => {
|
|||||||
mockDbGet.mockReturnValueOnce({ maps_api_key: 'gkey' });
|
mockDbGet.mockReturnValueOnce({ maps_api_key: 'gkey' });
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({ photos: [] }),
|
text: async () => JSON.stringify({ photos: [] }),
|
||||||
}));
|
}));
|
||||||
const { getPlacePhoto } = await import('../../../src/services/mapsService');
|
const { getPlacePhoto } = await import('../../../src/services/mapsService');
|
||||||
const noPhotoId = `ChIJNone-${Date.now()}`;
|
const noPhotoId = `ChIJNone-${Date.now()}`;
|
||||||
@@ -1281,7 +1281,7 @@ describe('getPlacePhoto (fetch stubbed)', () => {
|
|||||||
const fetchMock = vi.fn()
|
const fetchMock = vi.fn()
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
text: async () => JSON.stringify({
|
||||||
photos: [{ name: 'places/ChIJXYZ/photos/photo1', authorAttributions: [] }],
|
photos: [{ name: 'places/ChIJXYZ/photos/photo1', authorAttributions: [] }],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -1301,7 +1301,7 @@ describe('getPlacePhoto (fetch stubbed)', () => {
|
|||||||
const fetchMock = vi.fn()
|
const fetchMock = vi.fn()
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
text: async () => JSON.stringify({
|
||||||
photos: [{ name: 'places/ChIJNoAttr/photos/photo1', authorAttributions: [] }],
|
photos: [{ name: 'places/ChIJNoAttr/photos/photo1', authorAttributions: [] }],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user