fix: reliable concerts (backend events cache) + web importer preview layout
- Backend now caches Ticketmaster events per artist and prefers the larger recent result, so a flaky partial/empty response can't shrink a good list; every client (web/Android) stays consistent (1h TTL self-heals). - Web SoundCloud importer preview: add the missing .gtrack-num grid cell so the 4-column row no longer squeezes the cover/title (title was wrapping one char per line). - CHANGELOG [2.3] opened for this batch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f56d47800c
commit
b584159568
3 files changed
+69
-5
No files matched your search
@@ -370,10 +370,38 @@ app.get(/^\/api\/deezer\/(.+)/, (req, res) => {
|
||||
}).on('error', e => res.status(502).json({ error: e.message }));
|
||||
});
|
||||
|
||||
// Server-side events cache. Ticketmaster's free `keyword` search is flaky — for the same artist
|
||||
// it intermittently returns a partial subset (or []), so back-to-back calls disagree (web showed 7,
|
||||
// a later live call got only 2). Caching on the BACKEND (not per-client) keeps every client
|
||||
// consistent, and we PREFER THE LARGER recent result: a transient partial/empty response never
|
||||
// shrinks a good list. TTL bounds staleness (a genuinely-dropped show self-heals within an hour).
|
||||
const _eventsCache = new Map(); // artistLower → { ts, data }
|
||||
const _EVENTS_TTL = 60 * 60 * 1000; // 1h
|
||||
|
||||
app.get('/api/events/:artist', (req, res) => {
|
||||
const key = CONFIG.ticketmasterKey;
|
||||
if (!key) { console.warn('[events] TICKETMASTER_API_KEY not set'); return res.json([]); }
|
||||
const artist = req.params.artist;
|
||||
const cacheKey = artist.toLowerCase();
|
||||
const cached = _eventsCache.get(cacheKey);
|
||||
const fresh = cached && (Date.now() - cached.ts < _EVENTS_TTL);
|
||||
|
||||
// Serve the cached list, but only when it wouldn't hide a better live result. When we fall back
|
||||
// to cache on a TM error/partial, we DON'T bump ts — so the cache still expires on schedule and
|
||||
// self-heals instead of pinning stale events forever.
|
||||
const respondWithCacheFallback = (liveEvents) => {
|
||||
if (fresh && cached.data.length > liveEvents.length) {
|
||||
console.log(`[events] ${artist}: live=${liveEvents.length} < cache=${cached.data.length} → serving cache`);
|
||||
return res.json(cached.data);
|
||||
}
|
||||
if (liveEvents.length > 0 || !fresh) {
|
||||
_eventsCache.set(cacheKey, { ts: Date.now(), data: liveEvents });
|
||||
return res.json(liveEvents);
|
||||
}
|
||||
// live empty but cache still fresh → serve cache
|
||||
return res.json(cached.data);
|
||||
};
|
||||
|
||||
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'); // 2024-06-08T12:00:00Z
|
||||
const url = `https://app.ticketmaster.com/discovery/v2/events.json` +
|
||||
`?keyword=${encodeURIComponent(artist)}&classificationName=music` +
|
||||
@@ -387,7 +415,7 @@ app.get('/api/events/:artist', (req, res) => {
|
||||
const data = JSON.parse(body);
|
||||
if (upstream.statusCode !== 200 || data.fault || data.errors) {
|
||||
console.error('[events] Ticketmaster error:', upstream.statusCode, JSON.stringify(data).slice(0, 200));
|
||||
return res.json([]);
|
||||
return respondWithCacheFallback([]);
|
||||
}
|
||||
const raw = data?._embedded?.events || [];
|
||||
const events = raw.map(e => {
|
||||
@@ -411,11 +439,11 @@ app.get('/api/events/:artist', (req, res) => {
|
||||
offers: e.url ? [{ url: e.url, status: soldOut ? 'unavailable' : 'available' }] : [],
|
||||
};
|
||||
}).filter(e => e.datetime);
|
||||
console.log(`[events] ${artist}: ${events.length} events`);
|
||||
res.json(events);
|
||||
} catch(err) { console.error('[events] parse error:', err); res.json([]); }
|
||||
console.log(`[events] ${artist}: ${events.length} events (live)`);
|
||||
respondWithCacheFallback(events);
|
||||
} catch(err) { console.error('[events] parse error:', err); respondWithCacheFallback([]); }
|
||||
});
|
||||
}).on('error', e => { console.error('[events] request error:', e.message); res.json([]); });
|
||||
}).on('error', e => { console.error('[events] request error:', e.message); respondWithCacheFallback([]); });
|
||||
});
|
||||
|
||||
// Debug-only: returns raw Ticketmaster response so you can see exactly what the API sends back.
|
||||
|
||||
Reference in new issue
Block a user