Skip to content

Commit cb03582

Browse files
Dean SoferDean Sofer
authored andcommitted
Spotify embed player
1 parent b899c9e commit cb03582

6 files changed

Lines changed: 179 additions & 9 deletions

File tree

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
APIFY_TOKEN=apify_api_1234567890
2-
GOOGLE_TOKEN=your_google_maps_api_key_here
2+
GOOGLE_TOKEN=your_google_maps_api_key_here
3+
SPOTIFY_CLIENT_ID=your_spotify_client_id_here
4+
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here

.github/workflows/deploy.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ jobs:
2929
env:
3030
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
3131
GOOGLE_TOKEN: ${{ secrets.GOOGLE_TOKEN }}
32+
SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
33+
SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
3234
run: |
3335
sed -i "s/__APIFY_TOKEN__/${APIFY_TOKEN}/g" map.js
3436
sed -i "s/__GOOGLE_TOKEN__/${GOOGLE_TOKEN}/g" index.html
37+
sed -i "s/YOUR_SPOTIFY_CLIENT_ID/${SPOTIFY_CLIENT_ID}/g" 19hz/map.js
38+
sed -i "s/YOUR_SPOTIFY_CLIENT_SECRET/${SPOTIFY_CLIENT_SECRET}/g" 19hz/map.js
3539
3640
- name: Setup Pages
3741
uses: actions/configure-pages@v4

19hz/map.js

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
// Delimiter for multiple category selections (must not appear in category names and not require URI encoding)
33
const 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+
517
const 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
15111
let 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
}

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ The website is entirely static files
44
The site is hosted on github on the branch gh-pages branch The site is hosted at funcheapsfmap.com
55
Do not add comments, use full variable names instead of single letters, leave out optional characters when possible, only target the latest (evergreen) browsers on computer and mobile.
66
The website javascript downloads a json payload from an apify endpoint which is updated every other day by crawling funcheapsf.com and contains roughly 1500 blog pages crawled including event metadata and html.
7+
The 19hz subdirectory includes Spotify integration that requires SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET to be configured. The integration queries the Spotify API to get artist IDs and embeds the Spotify artist player in event info windows.
78
Use screenshots to demonstrate changes.

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@ Latest version has a button to quickly add events to your calendar of choice
1717
cp .env.example .env
1818
```
1919

20-
2. Add your Apify API token and Google Maps API key to `.env`:
20+
2. Add your API keys to `.env`:
2121
```
2222
APIFY_TOKEN=your_actual_apify_token_here
2323
GOOGLE_TOKEN=your_actual_google_maps_api_key_here
24+
SPOTIFY_CLIENT_ID=your_spotify_client_id_here
25+
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here
2426
```
27+
28+
**To get Spotify API credentials:**
29+
- Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard)
30+
- Log in with your Spotify account
31+
- Click "Create app"
32+
- Fill in app name and description
33+
- Copy the Client ID and Client Secret
2534

2635
3. Open `index.html` in your browser directly, or use a local server:
2736
```bash
@@ -36,14 +45,16 @@ The site automatically deploys to GitHub Pages when you push to the `gh-pages` b
3645

3746
**Required GitHub Secrets:**
3847

39-
You need to add your Apify API token and Google Maps API key as GitHub secrets:
48+
You need to add your API tokens as GitHub secrets:
4049

4150
1. Go to your repository on GitHub
4251
2. Navigate to **Settings****Secrets and variables****Actions**
4352
3. Click **New repository secret**
4453
4. Add the following secrets:
4554
- Name: `APIFY_TOKEN`, Value: Your Apify API token
4655
- Name: `GOOGLE_TOKEN`, Value: Your Google Maps API key
56+
- Name: `SPOTIFY_CLIENT_ID`, Value: Your Spotify Client ID
57+
- Name: `SPOTIFY_CLIENT_SECRET`, Value: Your Spotify Client Secret
4758
5. Click **Add secret** for each
4859

4960
The GitHub Actions workflow will automatically inject these tokens into the JavaScript files at build time.

style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ html, body, #map-canvas {
136136
transform: scale(0.95);
137137
}
138138
}
139+
#spotify-player {
140+
min-height: 152px;
141+
background: #f5f5f5;
142+
}
139143
}
140144

141145
/* google.maps.InfoWindow Body */

0 commit comments

Comments
 (0)