init commit - calling this project finished as of now, may revisit if in the future spotify lifts the premium requirement.

This commit is contained in:
2025-07-01 15:17:12 -05:00
commit d3285bb1f3
8 changed files with 398 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.vscode/
+51
View File
@@ -0,0 +1,51 @@
// Spotify OAuth logic
const client_id = '5ed07e2a23384be495efff4d463aba54';
const redirect_uri = window.location.origin + window.location.pathname;
const scope = 'user-read-private user-read-email user-read-playback-state user-modify-playback-state user-read-currently-playing';
function loginWithSpotify() {
const code_verifier = generateCodeVerifier();
localStorage.setItem('code_verifier', code_verifier);
generateCodeChallenge(code_verifier).then(code_challenge => {
const params = new URLSearchParams({
response_type: 'code',
client_id,
scope,
redirect_uri,
code_challenge_method: 'S256',
code_challenge
});
window.location = 'https://accounts.spotify.com/authorize?' + params.toString();
});
}
async function handleRedirect() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) return null;
const code_verifier = localStorage.getItem('code_verifier');
const body = new URLSearchParams({
client_id,
grant_type: 'authorization_code',
code,
redirect_uri,
code_verifier
});
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
const data = await response.json();
if (data.access_token) {
window.history.replaceState({}, document.title, window.location.pathname);
localStorage.removeItem('code_verifier');
localStorage.setItem('access_token', data.access_token);
return data.access_token;
} else {
showError('Error: ' + JSON.stringify(data, null, 2));
return null;
}
}
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Spotify Now Playing - YourName</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="icon" href="https://spotify.com/favicon.ico"/>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
</head>
<body class="bg-gradient-to-br from-green-400 via-black to-gray-900 min-h-screen flex flex-col items-center justify-center">
<div class="bg-white bg-opacity-10 rounded-xl shadow-xl p-8 flex flex-col items-center gap-4 w-full max-w-md">
<img id="profile-pic" class="rounded-full w-16 h-16 border-4 border-green-500" src="" alt="Profile" style="display:none;">
<h1 class="text-2xl font-bold text-white flex items-center gap-2">
<svg fill="#1DB954" viewport="0 0 40 40" class="w-6 h-6"><path d="M12 0C5.371 0 0 5.373 0 12c0 6.627 5.371 12 12 12 6.627 0 12-5.373 12-12C24 5.373 18.627 0 12 0zm5.41 17.817a.905.905 0 0 1-1.246.273c-3.408-2.087-7.705-2.556-12.76-1.395a.908.908 0 0 1-.418-1.77c5.497-1.298 10.219-.77 14.039 1.543a.905.905 0 0 1 .385 1.349zm1.715-3.193a1.13 1.13 0 0 1-1.559.342c-3.899-2.397-9.835-3.09-14.426-1.69a1.131 1.131 0 1 1-.655-2.166c5.135-1.553 11.573-.782 15.99 2.03a1.128 1.128 0 0 1 .35 1.484zm1.789-3.37C16.395 8.765 7.591 8.563 4.768 9.396a1.354 1.354 0 0 1-.727-2.613c3.249-.902 12.808-.67 16.824 2.083a1.353 1.353 0 0 1-1.67 2.388z"/></svg>
Spotify Now Playing
</h1>
<button id="login-btn" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Login with Spotify</button>
<div id="now-playing" class="w-full flex flex-col items-center" style="display:none;">
<img id="album-art" class="rounded-lg shadow w-40 h-40" src="" alt="Album Art"/>
<div class="mt-4 text-center">
<div id="track-title" class="text-xl font-bold text-white"></div>
<div id="track-artist" class="text-md text-gray-200"></div>
<div id="track-album" class="text-sm text-gray-400"></div>
</div>
<div class="w-full mt-4">
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div id="progress-bar" class="bg-green-400 h-2.5 rounded-full" style="width:0%"></div>
</div>
<div class="flex justify-between text-xs text-gray-300 mt-1">
<span id="progress-current">0:00</span>
<span id="progress-duration">0:00</span>
</div>
<div id="player-controls" class="flex gap-2 mt-4 justify-center items-center">
<button id="prev-btn"
class="btn border border-green-500 text-green-500 bg-transparent rounded-full px-3 py-1 text-base w-16 transition hover:bg-green-500 hover:text-white"
title="Previous"> <i class="fas fa-backward"></i> </button>
<button id="play-btn"
class="btn bg-green-500 text-white w-28 h-28 rounded-full text-2xl flex items-center justify-center transition"
title="Play"><i class="fas fa-play"></i></button>
<button id="pause-btn" style="display:none;"
class="btn bg-green-500 text-white w-28 h-28 rounded-full text-2xl flex items-center justify-center transition"
title="Pause"><i class="fas fa-pause"></i></button>
<button id="next-btn"
class="btn border border-green-500 text-green-500 bg-transparent rounded-full px-3 py-1 text-base w-16 transition hover:bg-green-500 hover:text-white"
title="Next"><i class="fas fa-forward"></i></button>
</div>
</div>
</div>
<div id="error-msg" class="text-red-500 font-bold"></div>
<footer class="mt-4 text-xs text-gray-400">Built by bckelley · <a href="https://github.com/bckelley/spotify-player" class="underline hover:text-green-300">Source</a></footer>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
import { loginWithSpotify, handleRedirect, getProfile, getCurrentTrack } from './spotify-api.js';
let isPremium = false;
const loginBtn = document.getElementById('login-btn');
const nowPlayingDiv = document.getElementById('now-playing');
const errorMsg = document.getElementById('error-msg');
const profilePic = document.getElementById('profile-pic');
loginBtn.onclick = loginWithSpotify;
async function main() {
const access_token = await handleRedirect() || localStorage.getItem('access_token');
if (!access_token) return;
loginBtn.style.display = 'none';
// Show user profile
const profile = await getProfile(access_token);
if (profile && profile.images && profile.images.length) {
profilePic.src = profile.images[0].url;
profilePic.style.display = '';
}
const isPremium = profile?.product === 'premium';
['play-btn', 'pause-btn', 'next-btn', 'prev-btn'].forEach(id => {
const btn = document.getElementById(id);
if (!isPremium) {
btn.disabled = true;
btn.title = 'Spotify Premium required for playback controls';
btn.classList.add('opacity-50', 'cursor-not-allowed');
}
});
const playBtn = document.getElementById('play-btn');
const pauseBtn = document.getElementById('pause-btn');
function showPause() {
playBtn.style.display = 'none';
pauseBtn.style.display = '';
}
function showPlay() {
playBtn.style.display = '';
pauseBtn.style.display = 'none';
}
function onPlaybackStateChanged(isPlaying) {
if (isPlaying) {
showPause();
} else {
showPlay();
}
}
// Show now playing
async function updateNowPlaying() {
const track = await getCurrentTrack(access_token);
if (!track || !track.item) {
nowPlayingDiv.style.display = 'none';
errorMsg.textContent = 'Nothing playing or no active device.';
return;
}
nowPlayingDiv.style.display = '';
errorMsg.textContent = '';
document.getElementById('album-art').src = track.item.album.images[0].url;
document.getElementById('track-title').textContent = track.item.name;
document.getElementById('track-artist').textContent = track.item.artists.map(a => a.name).join(', ');
document.getElementById('track-album').textContent = track.item.album.name;
// Progress bar
const progress = (track.progress_ms / track.item.duration_ms) * 100;
document.getElementById('progress-bar').style.width = `${progress}%`;
document.getElementById('progress-current').textContent = msToMinSec(track.progress_ms);
document.getElementById('progress-duration').textContent = msToMinSec(track.item.duration_ms);
onPlaybackStateChanged(track.is_playing);
}
setInterval(updateNowPlaying, 2000);
updateNowPlaying();
}
function msToMinSec(ms) {
const min = Math.floor(ms / 60000);
const sec = Math.floor((ms % 60000) / 1000);
return `${min}:${sec.toString().padStart(2, '0')}`;
}
main();
+14
View File
@@ -0,0 +1,14 @@
// PKCE utilities
function generateCodeVerifier(length = 64) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
+71
View File
@@ -0,0 +1,71 @@
# Spotify Now Playing
A simple web app to display your current Spotify playback, built with vanilla JS, Tailwind CSS, and the Spotify Web API.
## Features
- **Spotify OAuth Login** (PKCE flow, no backend required)
- **Displays current track** (title, artist, album, album art)
- **Playback controls** (play, pause, next, previous) for Spotify Premium users
- **Responsive, modern UI** using Tailwind CSS and Font Awesome icons
- **Shows playback progress bar**
- **Profile picture and user info**
## Requirements
- Spotify account (Premium required for playback controls)
- Modern browser (uses ES modules and Fetch API)
## Getting Started
1. **Clone this repo:**
```sh
git clone https://github.com/bckelley/spotify-player.git
cd spotify-player
```
2. **Set up a Spotify Developer App:**
- Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/applications)
- Create an app and set the Redirect URI to your local/test URL (e.g. `http://${app url/spotify/`)
- Copy your **Client ID** and update it in `spotify-api.js` and `auth.js` if needed.
3. **Run locally:**
- Place the project in your local web server root I used laragon (e.g. `c:\laragon\www\spotify`)
- Open `http://${app url}/` in your browser
4. **Login with Spotify:**
- Click the "Login with Spotify" button and authorize the app.
## File Structure
```
index.html # Main UI
main.js # App logic and UI updates
spotify-api.js # Spotify API and OAuth logic
auth.js # (Optional) Separate auth logic
ui.js # UI helper functions
pkce.js # PKCE utility functions
```
## Customization
- **Styling:** Uses [Tailwind CSS](https://tailwindcss.com/) via CDN for rapid styling.
- **Icons:** Uses [Font Awesome](https://fontawesome.com/) for playback controls.
- **Profile:** Shows your Spotify profile picture if available.
## Notes
- Playback controls (play, pause, next, previous) require a Spotify Premium account.
- The app polls the Spotify API every 2 seconds for updates.
- No backend server is required; all logic runs in the browser.
## Credits
- [Spotify Web API](https://developer.spotify.com/documentation/web-api/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Font Awesome](https://fontawesome.com/)
- Inspired by [bckelley/spotify-player](https://github.com/bckelley/spotify-player)
---
**Built by bckelley** · [Source](https://github.com/bckelley/spotify-player)
+95
View File
@@ -0,0 +1,95 @@
// Spotify Web API actions
function generateCodeVerifier(length = 128) {
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let code_verifier = '';
for (let i = 0; i < length; i++) {
code_verifier += possible.charAt(Math.floor(Math.random() * possible.length));
}
return code_verifier;
}
async function generateCodeChallenge(code_verifier) {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(code_verifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const client_id = '5ed07e2a23384be495efff4d463aba54';
const redirect_uri = window.location.origin + window.location.pathname;
const scope = 'user-read-private user-read-email user-read-playback-state user-read-currently-playing';
export function loginWithSpotify() {
const code_verifier = generateCodeVerifier();
localStorage.setItem('code_verifier', code_verifier);
generateCodeChallenge(code_verifier).then(code_challenge => {
const params = new URLSearchParams({
response_type: 'code',
client_id,
scope,
redirect_uri,
code_challenge_method: 'S256',
code_challenge
});
window.location = 'https://accounts.spotify.com/authorize?' + params.toString();
});
}
export async function handleRedirect() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) return null;
const code_verifier = localStorage.getItem('code_verifier');
const body = new URLSearchParams({
client_id,
grant_type: 'authorization_code',
code,
redirect_uri,
code_verifier
});
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
const data = await response.json();
if (data.access_token) {
window.history.replaceState({}, document.title, window.location.pathname);
localStorage.removeItem('code_verifier');
localStorage.setItem('access_token', data.access_token);
return data.access_token;
} else {
document.getElementById('error-msg').textContent = 'Error: ' + JSON.stringify(data, null, 2);
return null;
}
}
// --- Profile and Current Track ---
export async function getProfile(access_token) {
const res = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: 'Bearer ' + access_token }
});
if (!res.ok) return null;
return await res.json();
}
export async function getCurrentTrack(access_token) {
const res = await fetch('https://api.spotify.com/v1/me/player/currently-playing', {
headers: { Authorization: 'Bearer ' + access_token }
});
if (res.status === 204 || !res.ok) return null;
return await res.json();
}
async function playerCommand(access_token, endpoint, method='POST') {
const response = await fetch(`https://api.spotify.com/v1/me/player/${endpoint}`, {
method,
headers: { Authorization: 'Bearer ' + access_token }
});
if (!response.ok) {
const text = await response.text();
document.getElementById('data').innerText = `Spotify API Error (${endpoint}): ${response.status} ${text}`;
console.error(`Spotify API Error (${endpoint}):`, response.status, text);
}
}
+19
View File
@@ -0,0 +1,19 @@
function showError(msg) {
document.getElementById('data').innerText = msg;
}
function updateTrackInfo(track) {
if (!track) {
document.getElementById('track-info').innerText = 'Nothing playing or no active device.';
return;
}
if (track.item) {
document.getElementById('track-info').innerText =
`Now Playing: ${track.item.name} by ${track.item.artists.map(a => a.name).join(", ")}`;
} else {
document.getElementById('track-info').innerText = 'Nothing playing.';
}
}
function showControls() {
document.getElementById('controls').style.display = '';
document.getElementById('login-btn').style.display = 'none';
}