22// Delimiter for multiple category selections (must not appear in category names and not require URI encoding)
33const CATEGORY_DELIMITER = '~' ;
44
5+ // Spotify API configuration
6+ // For production, these should be loaded from environment variables or a secure backend
7+ const SPOTIFY_CLIENT_ID = 'YOUR_SPOTIFY_CLIENT_ID' ;
8+ const SPOTIFY_CLIENT_SECRET = 'YOUR_SPOTIFY_CLIENT_SECRET' ;
9+
10+ // Cache for Spotify access token
11+ let spotifyAccessToken = null ;
12+ let spotifyTokenExpiry = null ;
13+
14+ // Cache for artist IDs to avoid repeated API calls
15+ const artistIdCache = new Map ( ) ;
16+
517const intersectionObserver = new IntersectionObserver ( ( entries ) => {
618 for ( const entry of entries ) {
719 if ( entry . isIntersecting ) {
@@ -11,6 +23,90 @@ const intersectionObserver = new IntersectionObserver((entries) => {
1123 }
1224} ) ;
1325
26+ /**
27+ * Gets a Spotify access token using client credentials flow
28+ * @returns {Promise<string> } Access token
29+ */
30+ async function getSpotifyAccessToken ( ) {
31+ if ( SPOTIFY_CLIENT_ID === 'YOUR_SPOTIFY_CLIENT_ID' ||
32+ SPOTIFY_CLIENT_SECRET === 'YOUR_SPOTIFY_CLIENT_SECRET' ) {
33+ console . warn ( 'Spotify API credentials not configured. Please add SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET.' ) ;
34+ return null ;
35+ }
36+
37+ if ( spotifyAccessToken && spotifyTokenExpiry && Date . now ( ) < spotifyTokenExpiry ) {
38+ return spotifyAccessToken ;
39+ }
40+
41+ try {
42+ const response = await fetch ( 'https://accounts.spotify.com/api/token' , {
43+ method : 'POST' ,
44+ headers : {
45+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
46+ 'Authorization' : 'Basic ' + btoa ( SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET )
47+ } ,
48+ body : 'grant_type=client_credentials'
49+ } ) ;
50+
51+ if ( ! response . ok ) {
52+ throw new Error ( `Spotify auth failed: ${ response . status } ` ) ;
53+ }
54+
55+ const data = await response . json ( ) ;
56+ spotifyAccessToken = data . access_token ;
57+ spotifyTokenExpiry = Date . now ( ) + ( data . expires_in * 1000 ) - 60000 ;
58+
59+ return spotifyAccessToken ;
60+ } catch ( error ) {
61+ console . error ( 'Failed to get Spotify access token:' , error ) ;
62+ return null ;
63+ }
64+ }
65+
66+ /**
67+ * Searches Spotify for an artist and returns their ID
68+ * @param {string } artistName - Name of the artist to search for
69+ * @returns {Promise<string|null> } Spotify artist ID or null if not found
70+ */
71+ async function searchSpotifyArtist ( artistName ) {
72+ if ( artistIdCache . has ( artistName ) ) {
73+ return artistIdCache . get ( artistName ) ;
74+ }
75+
76+ const token = await getSpotifyAccessToken ( ) ;
77+ if ( ! token ) {
78+ return null ;
79+ }
80+
81+ try {
82+ const response = await fetch (
83+ `https://api.spotify.com/v1/search?q=${ encodeURIComponent ( artistName ) } &type=artist&limit=1` ,
84+ {
85+ headers : {
86+ 'Authorization' : `Bearer ${ token } `
87+ }
88+ }
89+ ) ;
90+
91+ if ( ! response . ok ) {
92+ throw new Error ( `Spotify search failed: ${ response . status } ` ) ;
93+ }
94+
95+ const data = await response . json ( ) ;
96+
97+ if ( data . artists && data . artists . items && data . artists . items . length > 0 ) {
98+ const artistId = data . artists . items [ 0 ] . id ;
99+ artistIdCache . set ( artistName , artistId ) ;
100+ return artistId ;
101+ }
102+
103+ return null ;
104+ } catch ( error ) {
105+ console . error ( `Failed to search for artist "${ artistName } ":` , error ) ;
106+ return null ;
107+ }
108+ }
109+
14110// Global variable to track user location marker
15111let userLocationMarker = null ;
16112
@@ -369,10 +465,57 @@ window.viewEventDetails = function(eventIndex) {
369465 * Updates the Spotify player to search for a specific artist
370466 * @param {string } artistName - The artist name to search for
371467 */
372- window . updateSpotifyPlayer = function ( artistName ) {
468+ window . updateSpotifyPlayer = async function ( artistName ) {
373469 const spotifyPlayer = document . getElementById ( 'spotify-player' ) ;
374- if ( spotifyPlayer ) {
375- spotifyPlayer . src = `https://open.spotify.com/embed/search/${ encodeURIComponent ( artistName ) } ` ;
470+ if ( ! spotifyPlayer ) {
471+ return ;
472+ }
473+
474+ const container = spotifyPlayer . parentElement ;
475+
476+ // Show loading state
477+ spotifyPlayer . style . display = 'none' ;
478+ if ( ! container . querySelector ( '.spotify-loading' ) ) {
479+ const loadingDiv = document . createElement ( 'div' ) ;
480+ loadingDiv . className = 'spotify-loading' ;
481+ loadingDiv . style . cssText = 'text-align: center; padding: 20px; color: #666; font-size: 14px;' ;
482+ loadingDiv . textContent = `Loading ${ artistName } ...` ;
483+ container . appendChild ( loadingDiv ) ;
484+ }
485+
486+ try {
487+ const artistId = await searchSpotifyArtist ( artistName ) ;
488+
489+ const loadingDiv = container . querySelector ( '.spotify-loading' ) ;
490+ if ( loadingDiv ) {
491+ loadingDiv . remove ( ) ;
492+ }
493+
494+ if ( artistId ) {
495+ // Use the proper artist embed URL with the artist ID
496+ spotifyPlayer . src = `https://open.spotify.com/embed/artist/${ artistId } ` ;
497+ spotifyPlayer . style . display = 'block' ;
498+ } else {
499+ console . warn ( `Could not find Spotify artist ID for: ${ artistName } ` ) ;
500+ spotifyPlayer . style . display = 'none' ;
501+
502+ // Show error message
503+ const errorDiv = document . createElement ( 'div' ) ;
504+ errorDiv . className = 'spotify-error' ;
505+ errorDiv . style . cssText = 'text-align: center; padding: 20px; color: #999; font-size: 12px;' ;
506+ errorDiv . textContent = `Artist "${ artistName } " not found on Spotify` ;
507+ container . appendChild ( errorDiv ) ;
508+
509+ // Remove error after 3 seconds
510+ setTimeout ( ( ) => errorDiv . remove ( ) , 3000 ) ;
511+ }
512+ } catch ( error ) {
513+ console . error ( `Error updating Spotify player for ${ artistName } :` , error ) ;
514+ const loadingDiv = container . querySelector ( '.spotify-loading' ) ;
515+ if ( loadingDiv ) {
516+ loadingDiv . remove ( ) ;
517+ }
518+ spotifyPlayer . style . display = 'none' ;
376519 }
377520} ;
378521
@@ -692,11 +835,11 @@ class Events {
692835 return `<a href="https://open.spotify.com/search/${ encodeURIComponent ( artist ) } " target="_blank">${ artist } </a>${ speakerIcon } ` ;
693836 } ) . join ( ', ' ) ;
694837
695- // Create Spotify embed player
838+ // Create Spotify embed player placeholder
696839 spotifyPlayerHTML = `
697840 <iframe id="spotify-player"
698841 style="border-radius: 12px; margin-top: 10px;"
699- src="https://open.spotify.com/embed/search/ ${ encodeURIComponent ( firstArtist ) } "
842+ src=""
700843 width="100%"
701844 height="152"
702845 frameBorder="0"
@@ -712,14 +855,19 @@ class Events {
712855 <p><strong>Genres:</strong> ${ event . categories . map ( category => `<a onclick="filter({category:'${ category } '})">${ category } </a>` ) . join ( ', ' ) } </p>
713856 ${ hasValidArtists ? `<p><strong>Artists:</strong> ${ artistsHTML } </p>` : '' }
714857 <p><strong>Age:</strong> ${ ageInfo } </p>
715- ${ spotifyPlayerHTML }
716858 </div>
717859 <div class="info-body">
718860 ${ spotifyPlayerHTML }
719861 </div>
720862 ` ;
721863
722864 this . cachedInfoWindow . setContent ( content ) ;
865+
866+ // Load first artist after content is set
867+ if ( hasValidArtists ) {
868+ const artists = artistsInfo . split ( ',' ) . map ( artist => artist . trim ( ) ) ;
869+ updateSpotifyPlayer ( artists [ 0 ] ) ;
870+ }
723871 }
724872 return this . cachedInfoWindow ;
725873 }
0 commit comments