diff --git a/CHANGELOG.md b/CHANGELOG.md index c94c829..790491c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 3eb563e..5e3f32f 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -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. diff --git a/src/views/soundcloud.js b/src/views/soundcloud.js index 6de8eef..394cbe8 100644 --- a/src/views/soundcloud.js +++ b/src/views/soundcloud.js @@ -138,6 +138,7 @@ function _renderSoundCloudPreview(data) { const artT = t.artworkUrl || ''; el.innerHTML = `