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
@@ -64,6 +64,41 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an
|
||||
|
||||
---
|
||||
|
||||
## [2.3] — 2026-08-21
|
||||
|
||||
A cleanup-and-parity release: the **Song Importer moves into the Global Search field** (one field,
|
||||
like Android), concert events become **reliable across web/Android**, and several Android gaps close.
|
||||
|
||||
### Added
|
||||
- **Unified Global Search field (Web)** — the separate Song-Importer page/sidebar entry is gone. The
|
||||
one search field now auto-detects: a **link** (Spotify/SoundCloud/YouTube) opens the importer
|
||||
preview, plain **text** runs a Deezer search. Cleaner left nav, same behaviour as Android.
|
||||
|
||||
### Fixed
|
||||
- **Concert events reliability (Web + Android)** — Ticketmaster's free `keyword` search intermittently
|
||||
returns a partial subset (or none), so back-to-back calls disagreed (web showed 7, a later live
|
||||
call got only 2, Android sometimes showed 0). The web had a private 30-min cache masking this; the
|
||||
cache now lives **on the backend** and **prefers the larger recent result**, so a flaky response
|
||||
can't shrink a good list and every client stays consistent (1 h TTL self-heals).
|
||||
- **Concert date/time (Web + Android)** — shows the **venue-local** time (Ticketmaster's `localDate`/
|
||||
`localTime`) instead of the UTC instant, so a 20:00 show no longer displayed as 08:00. Android
|
||||
parses the naive local datetime (was crashing on zone-less values → missing times).
|
||||
- **Concert rows uneven (Android)** — rebuilt each row with a fixed-width calendar-style date block
|
||||
(day big, month/year + time small), so every row is the same height and the divider/venue align.
|
||||
- **Concert load failures were silent (Android)** — the fetch swallowed errors and showed an empty
|
||||
list. Errors now surface ("Konzerte konnten nicht geladen werden" + reason), with a request
|
||||
timeout and non-2xx treated as a real failure.
|
||||
- **Web importer preview layout** — the SoundCloud track row was missing its grid number-cell, so the
|
||||
4-column `.gtrack-item` grid squeezed the cover into 28px and the title into 52px (title wrapped
|
||||
one character per line). Added the missing cell.
|
||||
- **Android artist albums incomplete** — `lookupArtistAlbums` filtered to `record_type == "album"`,
|
||||
dropping EPs and singles that Deezer (and the web app) show. Now returns all, sorted album → EP →
|
||||
single.
|
||||
- **"Falscher Künstler?" pin button minimised (Web + Android)** — reduced to a small ⚙ icon-only pill
|
||||
in the artist hero's corner instead of a full-width labelled button.
|
||||
|
||||
---
|
||||
|
||||
## [2.2] — 2026-08-19
|
||||
|
||||
Gets YouTube downloads working again after they broke on two independent fronts.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -138,6 +138,7 @@ function _renderSoundCloudPreview(data) {
|
||||
const artT = t.artworkUrl || '';
|
||||
el.innerHTML = `
|
||||
<div class="gtrack-art">${artT ? `<img src="${artT}" loading="lazy">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'}</div>
|
||||
<div class="gtrack-num">${i + 1}</div>
|
||||
<div class="gtrack-info">
|
||||
<div class="gtrack-title">${esc(t.trackName || '—')}</div>
|
||||
<div class="gtrack-sub">${esc(t.artistName || '—')}${t.genre ? ' · ' + esc(t.genre) : ''}${t.duration ? ' · ' + Math.round(t.duration / 60) + ' min' : ''}</div>
|
||||
|
||||
Reference in new issue
Block a user