From adcbb2e883df5e31dd39bb581bbf2af9e9311a6e Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 8 Jun 2026 20:51:51 +0200 Subject: [PATCH] feat: replace Bandsintown with Ticketmaster for concert events Backend: swap /api/events/:artist to use Ticketmaster Discovery API (startDateTime=now, classificationName=music, size=20, sort by date). Maps TM response to the same {datetime, venue, offers} shape the modal expects. Returns [] if TICKETMASTER_API_KEY is not set. Frontend: remove all debug console.logs from events.js; drop redundant client-side date filter (TM already filters by startDateTime server-side). Co-Authored-By: Claude Sonnet 4.6 --- backend/lib/config.js | 3 ++- backend/novasonic-server.js | 46 +++++++++++++++++++++++++++++-------- src/views/events.js | 9 ++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/backend/lib/config.js b/backend/lib/config.js index 4a35478..cb08d2d 100644 --- a/backend/lib/config.js +++ b/backend/lib/config.js @@ -9,7 +9,8 @@ const CONFIG = { // 'ytdlp' | 'squid' | 'auto' (squid first, yt-dlp fallback) downloadBackend: (process.env.DOWNLOAD_BACKEND || 'auto').toLowerCase(), // squid.wtf quality: 'mp3' | 'flac' | 'hi-res' - squidQuality: (process.env.SQUID_QUALITY || 'mp3').toLowerCase(), + squidQuality: (process.env.SQUID_QUALITY || 'mp3').toLowerCase(), + ticketmasterKey: process.env.TICKETMASTER_API_KEY || '', }; module.exports = CONFIG; diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 4029fba..3d3bcc1 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -316,17 +316,43 @@ app.get('/api/itunes/:endpoint', (req, res) => { }); app.get('/api/events/:artist', (req, res) => { + const key = CONFIG.ticketmasterKey; + if (!key) return res.json([]); const artist = req.params.artist; - const target = `https://rest.bandsintown.com/artists/${encodeURIComponent(artist)}/events?app_id=novasonic`; - https.get(target, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept': 'application/json', - } - }, upstream => { - res.setHeader('Content-Type', 'application/json'); - upstream.pipe(res); - }).on('error', e => res.status(502).json({ error: e.message })); + const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'); + const url = `https://app.ticketmaster.com/discovery/v2/events.json` + + `?keyword=${encodeURIComponent(artist)}&classificationName=music` + + `&startDateTime=${encodeURIComponent(now)}&size=20&sort=date%2Casc` + + `&apikey=${encodeURIComponent(key)}`; + https.get(url, { headers: { 'Accept': 'application/json' } }, upstream => { + let body = ''; + upstream.on('data', d => body += d); + upstream.on('end', () => { + try { + const data = JSON.parse(body); + const raw = data?._embedded?.events || []; + const events = raw.map(e => { + const venue = e._embedded?.venues?.[0] || {}; + const start = e.dates?.start || {}; + const dt = start.dateTime || (start.localDate + ? `${start.localDate}T${start.localTime || '00:00:00'}` + : null); + const soldOut = e.dates?.status?.code === 'offsale' || + e.dates?.status?.code === 'cancelled'; + return { + datetime: dt, + venue: { + name: venue.name || '', + city: venue.city?.name || '', + country: venue.country?.name || '', + }, + offers: e.url ? [{ url: e.url, status: soldOut ? 'unavailable' : 'available' }] : [], + }; + }).filter(e => e.datetime); + res.json(events); + } catch { res.json([]); } + }); + }).on('error', () => res.json([])); }); app.get('/api/health', (req, res) => { diff --git a/src/views/events.js b/src/views/events.js index 48dfa56..bd8a0f1 100644 --- a/src/views/events.js +++ b/src/views/events.js @@ -3,19 +3,14 @@ import { S } from '../state.js'; export async function fetchEvents(artistName) { const base = S.cfg?.backendUrl; - console.log('[events] base:', base, '| artist:', artistName); if(!base) return []; try { const r = await fetch(`${base}/api/events/${encodeURIComponent(artistName)}`); - console.log('[events] status:', r.status); if(!r.ok) return []; const data = await r.json(); - console.log('[events] data:', data); if(!Array.isArray(data)) return []; - const filtered = data.filter(e => new Date(e.datetime) > new Date()); - console.log('[events] upcoming:', filtered.length); - return filtered; - } catch(e) { console.log('[events] error:', e); return []; } + return data; + } catch { return []; } } export function openEventsModal(events, artistName) {