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:
HoneyandClaude Opus 4.8 committed 2026-08-21 23:02:56 +02:00
1 parent f56d47800c
commit b584159568
3 files changed
+69 -5

No files matched your search

+33 -5
View File
@@ -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.