spotify / public /script.js
yetarnished's picture
Update public/script.js
986a31d verified
Raw
History Blame Contribute Delete
21.9 kB
class SpotifyPlayer {
constructor() {
this.token = null;
this.player = null;
this.deviceId = null;
this.currentTrack = null;
this.isPlaying = false;
this.currentPosition = 0;
this.duration = 0;
this.volume = 0.5;
this.queue = [];
this.currentQueueIndex = -1;
this.isRadioMode = false;
this.playbackContext = null;
this.init();
}
init() {
this.checkToken();
this.setupEventListeners();
}
checkToken() {
// Check localStorage first
const storedToken = localStorage.getItem('spotify_access_token');
if (storedToken) {
this.token = storedToken;
this.showMainApp();
return;
}
// Check URL hash
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const token = params.get('access_token');
if (token) {
this.token = token;
localStorage.setItem('spotify_access_token', token);
window.location.hash = '';
this.showMainApp();
} else {
this.showLoginScreen();
}
}
showLoginScreen() {
document.getElementById('login-screen').classList.remove('hidden');
document.getElementById('main-app').classList.add('hidden');
}
async showMainApp() {
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('main-app').classList.remove('hidden');
await this.initializeSpotifyPlayer();
await this.loadUserProfile();
await this.loadLikedSongs();
}
async initializeSpotifyPlayer() {
return new Promise((resolve) => {
window.onSpotifyWebPlaybackSDKReady = () => {
this.player = new Spotify.Player({
name: 'Spotify Web Player',
getOAuthToken: cb => { cb(this.token); },
volume: this.volume
});
// Error handling
this.player.addListener('initialization_error', ({ message }) => {
console.error('Failed to initialize:', message);
});
this.player.addListener('authentication_error', ({ message }) => {
console.error('Failed to authenticate:', message);
this.logout();
});
this.player.addListener('account_error', ({ message }) => {
console.error('Failed to validate Spotify account:', message);
});
this.player.addListener('playback_error', ({ message }) => {
console.error('Failed to perform playback:', message);
});
// Playback status updates
this.player.addListener('player_state_changed', (state) => {
if (!state) return;
this.currentTrack = state.track_window.current_track;
this.isPlaying = !state.paused;
this.currentPosition = state.position;
this.duration = state.duration;
// Store context for next/previous functionality
if (state.context && state.context.uri) {
this.playbackContext = state.context;
}
this.updateUI();
// Check if track ended
if (state.paused && state.position === 0 && this.previousPosition > 0) {
this.handleTrackEnd();
}
this.previousPosition = state.position;
});
// Ready
this.player.addListener('ready', ({ device_id }) => {
console.log('Ready with Device ID', device_id);
this.deviceId = device_id;
resolve();
});
// Connect to the player!
this.player.connect();
};
});
}
async loadUserProfile() {
try {
const response = await fetch('https://api.spotify.com/v1/me', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const user = await response.json();
document.getElementById('user-name').textContent = user.display_name || 'User';
if (user.images && user.images.length > 0) {
document.getElementById('user-avatar').src = user.images[0].url;
}
} catch (error) {
console.error('Error loading user profile:', error);
}
}
async loadLikedSongs() {
try {
const response = await fetch('https://api.spotify.com/v1/me/tracks?limit=50', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
const container = document.getElementById('liked-songs');
container.innerHTML = '';
if (data.items && data.items.length > 0) {
// Store liked songs as a playlist
this.likedSongs = data.items.map(item => item.track);
data.items.forEach((item, index) => {
const trackElement = this.createTrackElement(item.track, true, index);
container.appendChild(trackElement);
});
} else {
container.innerHTML = '<div class="loading">No liked songs found</div>';
}
} catch (error) {
console.error('Error loading liked songs:', error);
document.getElementById('liked-songs').innerHTML = '<div class="loading">Error loading liked songs</div>';
}
}
async search(query) {
if (!query.trim()) return;
this.showLoading();
try {
const response = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`, {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
const container = document.getElementById('search-results');
container.innerHTML = '';
if (data.tracks && data.tracks.items.length > 0) {
data.tracks.items.forEach(track => {
const trackElement = this.createTrackElement(track);
container.appendChild(trackElement);
});
} else {
container.innerHTML = '<div class="loading">No results found</div>';
}
} catch (error) {
console.error('Error searching:', error);
document.getElementById('search-results').innerHTML = '<div class="loading">Error searching</div>';
}
this.hideLoading();
}
createTrackElement(track, isLiked = false, index = -1) {
const div = document.createElement('div');
div.className = 'track-item';
if (this.currentTrack && this.currentTrack.id === track.id) {
div.classList.add('playing');
}
div.innerHTML = `
<img src="${track.album.images[2]?.url || track.album.images[0]?.url}" alt="${track.name}" class="track-image">
<div class="track-info">
<div class="track-name">${track.name}</div>
<div class="track-artist">${track.artists.map(a => a.name).join(', ')}</div>
</div>
<button class="like-btn ${isLiked ? 'liked' : ''}" data-track-id="${track.id}">
${isLiked ? '♥' : '♡'}
</button>
`;
div.addEventListener('click', (e) => {
if (!e.target.classList.contains('like-btn')) {
if (isLiked && index >= 0) {
// Playing from liked songs - set up queue
this.playFromLikedSongs(index);
} else {
// Playing from search - enable radio mode
this.playTrack(track, true);
}
}
});
div.querySelector('.like-btn').addEventListener('click', (e) => {
e.stopPropagation();
this.toggleLike(track.id, e.target);
});
return div;
}
async playFromLikedSongs(index) {
if (!this.likedSongs || index >= this.likedSongs.length) return;
try {
// Play all liked songs starting from the selected one
const uris = this.likedSongs.slice(index).map(track => track.uri);
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, {
method: 'PUT',
body: JSON.stringify({
uris: uris
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
}
});
this.isRadioMode = false;
this.queue = this.likedSongs.slice(index);
this.currentQueueIndex = 0;
} catch (error) {
console.error('Error playing from liked songs:', error);
}
}
async playTrack(track, startRadioMode = true) {
try {
if (startRadioMode) {
// Get recommendations first
await this.loadSimilarTracks(track);
// Play the queue if we have recommendations
if (this.queue.length > 0) {
const uris = this.queue.map(t => t.uri);
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, {
method: 'PUT',
body: JSON.stringify({
uris: uris
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
}
});
this.isRadioMode = true;
this.currentQueueIndex = 0;
} else {
// Fallback to single track if recommendations fail
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, {
method: 'PUT',
body: JSON.stringify({
uris: [track.uri]
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
}
});
this.isRadioMode = false;
}
} else {
// Just play single track
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${this.deviceId}`, {
method: 'PUT',
body: JSON.stringify({
uris: [track.uri]
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
}
});
}
this.currentTrack = track;
this.updateUI();
} catch (error) {
console.error('Error playing track:', error);
}
}
async loadSimilarTracks(seedTrack) {
try {
const response = await fetch('/recommendations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access_token: this.token,
seed_tracks: seedTrack.id,
seed_artists: seedTrack.artists[0].id
})
});
if (response.ok) {
const data = await response.json();
if (data.tracks) {
this.queue = [seedTrack, ...data.tracks];
this.currentQueueIndex = 0;
return;
}
}
// Fallback - just use the seed track
this.queue = [seedTrack];
this.currentQueueIndex = 0;
} catch (error) {
console.error('Error loading similar tracks:', error);
// Fallback - just use the seed track
this.queue = [seedTrack];
this.currentQueueIndex = 0;
}
}
async handleTrackEnd() {
// This is handled by the SDK's queue now
}
async togglePlayPause() {
try {
if (this.isPlaying) {
await this.player.pause();
} else {
await this.player.resume();
}
} catch (error) {
console.error('Error toggling play/pause:', error);
}
}
async nextTrack() {
try {
await this.player.nextTrack();
} catch (error) {
console.error('Error skipping to next track:', error);
}
}
async previousTrack() {
try {
await this.player.previousTrack();
} catch (error) {
console.error('Error skipping to previous track:', error);
}
}
async setVolume(volume) {
this.volume = volume / 100;
try {
await this.player.setVolume(this.volume);
} catch (error) {
console.error('Error setting volume:', error);
}
}
async toggleLike(trackId, button) {
const isLiked = button.classList.contains('liked');
try {
const method = isLiked ? 'DELETE' : 'PUT';
await fetch(`https://api.spotify.com/v1/me/tracks?ids=${trackId}`, {
method: method,
headers: { 'Authorization': `Bearer ${this.token}` }
});
button.classList.toggle('liked');
button.textContent = button.classList.contains('liked') ? '♥' : '♡';
// Update current track like button if it's the same track
if (this.currentTrack && this.currentTrack.id === trackId) {
const currentLikeBtn = document.getElementById('like-current-btn');
currentLikeBtn.classList.toggle('liked');
currentLikeBtn.textContent = currentLikeBtn.classList.contains('liked') ? '♥' : '♡';
}
// Reload liked songs if we're on that tab
const likedTab = document.getElementById('liked-tab');
if (likedTab.classList.contains('active')) {
await this.loadLikedSongs();
}
} catch (error) {
console.error('Error toggling like:', error);
}
}
updateUI() {
// Update current track info
if (this.currentTrack) {
document.getElementById('current-track-info').classList.remove('hidden');
document.getElementById('current-track-image').src = this.currentTrack.album.images[2]?.url || this.currentTrack.album.images[0]?.url;
document.getElementById('current-track-name').textContent = this.currentTrack.name;
document.getElementById('current-track-artist').textContent = this.currentTrack.artists.map(a => a.name).join(', ');
// Check if current track is liked
this.checkIfTrackIsLiked(this.currentTrack.id);
}
// Update play button
document.getElementById('play-btn').textContent = this.isPlaying ? '⏸️' : '▶️';
// Update progress
const progressPercent = this.duration ? (this.currentPosition / this.duration) * 100 : 0;
document.getElementById('progress').style.width = progressPercent + '%';
// Update time display
document.getElementById('current-time').textContent = this.formatTime(this.currentPosition);
document.getElementById('total-time').textContent = this.formatTime(this.duration);
// Update playing indicators
document.querySelectorAll('.track-item').forEach(item => {
item.classList.remove('playing');
});
if (this.currentTrack) {
document.querySelectorAll('.track-item').forEach(item => {
const trackName = item.querySelector('.track-name').textContent;
if (trackName === this.currentTrack.name) {
item.classList.add('playing');
}
});
}
}
async checkIfTrackIsLiked(trackId) {
try {
const response = await fetch(`https://api.spotify.com/v1/me/tracks/contains?ids=${trackId}`, {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const [isLiked] = await response.json();
const currentLikeBtn = document.getElementById('like-current-btn');
if (isLiked) {
currentLikeBtn.classList.add('liked');
currentLikeBtn.textContent = '♥';
} else {
currentLikeBtn.classList.remove('liked');
currentLikeBtn.textContent = '♡';
}
} catch (error) {
console.error('Error checking if track is liked:', error);
}
}
formatTime(ms) {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
logout() {
localStorage.removeItem('spotify_access_token');
this.token = null;
if (this.player) {
this.player.disconnect();
}
this.showLoginScreen();
}
setupEventListeners() {
// Login
document.getElementById('login-btn').addEventListener('click', () => {
window.location.href = '/login';
});
// Logout
document.getElementById('logout-btn').addEventListener('click', () => {
this.logout();
});
// Tab switching
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
// Update active tab
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Show corresponding content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
});
});
// Search
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const performSearch = () => {
this.search(searchInput.value);
};
searchBtn.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
// Player controls
document.getElementById('play-btn').addEventListener('click', () => {
this.togglePlayPause();
});
document.getElementById('next-btn').addEventListener('click', () => {
this.nextTrack();
});
document.getElementById('prev-btn').addEventListener('click', () => {
this.previousTrack();
});
// Volume control
document.getElementById('volume-slider').addEventListener('input', (e) => {
this.setVolume(e.target.value);
});
// Progress bar click
document.querySelector('.progress-bar').addEventListener('click', async (e) => {
const rect = e.target.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
const position = percent * this.duration;
try {
await this.player.seek(position);
} catch (error) {
console.error('Error seeking:', error);
}
});
// Current track like button
document.getElementById('like-current-btn').addEventListener('click', () => {
if (this.currentTrack) {
this.toggleLike(this.currentTrack.id, document.getElementById('like-current-btn'));
}
});
// Update progress periodically
setInterval(() => {
if (this.isPlaying) {
this.player.getCurrentState().then(state => {
if (state) {
this.currentPosition = state.position;
this.updateUI();
}
});
}
}, 1000);
}
}
// Initialize the app
const spotifyPlayer = new SpotifyPlayer();