let currentTrack = null;
let isPlaying = false;
let playlist = [];
let currentView = 'all';
let playlists = JSON.parse(localStorage.getItem('playlists')) || {};
let currentLyrics = null;
let isPlayingPlaylist = false;
// Add these cache objects at the top of the file
const ALBUM_CACHE = new Map();
const ARTIST_CACHE = new Map();
let lastStructureUpdate = 0;
const audioPlayer = document.getElementById('audio-player');
const playBtn = document.getElementById('play-btn');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const seekSlider = document.getElementById('seek-slider');
const volumeSlider = document.getElementById('volume-slider');
const currentTimeSpan = document.getElementById('current-time');
const durationSpan = document.getElementById('duration');
// Add this constant at the top of the file
const DEFAULT_COVER = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg==';
// Add this function to check if we need to rebuild caches
function shouldRebuildCaches() {
const now = Date.now();
if (now - lastStructureUpdate > 5000) { // Only rebuild every 5 seconds max
lastStructureUpdate = now;
return true;
}
return false;
}
// Fetch music structure from the server
fetch('/api/music-structure')
.then(response => response.json())
.then(data => {
playlist = flattenPlaylist(data);
initializeViews();
});
function flattenPlaylist(structure, prefix = '') {
let flat = [];
structure.forEach(item => {
if (item.type === 'file') {
item.fullPath = prefix + item.path;
flat.push(item);
} else if (item.type === 'folder') {
flat = flat.concat(flattenPlaylist(item.contents, item.path + '/'));
}
});
return flat;
}
function playTrack(index) {
// If playing directly from main playlist, clear playlist context
if (!isPlayingPlaylist) {
currentPlaylist = null;
}
currentTrack = index;
const track = playlist[index];
if (!track) return;
audioPlayer.src = `/music/${track.path}`;
audioPlayer.play()
.then(() => {
updatePlayerInfo(track);
updatePlayButton(true);
highlightCurrentTrack();
updateActiveLyricLine(audioPlayer.currentTime);
})
.catch(error => {
console.error('Error playing track:', error);
showNotification('Error playing track');
playNext();
});
}
function updatePlayerInfo(track) {
document.getElementById('track-title').textContent = track.metadata?.title || track.name;
document.getElementById('track-artist').textContent = track.metadata?.artist || 'Unknown';
document.getElementById('track-album').textContent = track.metadata?.album || 'Unknown';
// Update album art with error handling
const albumArt = document.getElementById('album-art');
if (track.metadata?.artwork) {
albumArt.src = track.metadata.artwork;
// Add error handling for the album art
albumArt.onerror = () => {
albumArt.src = DEFAULT_COVER;
};
} else {
albumArt.src = DEFAULT_COVER;
}
// Update lyrics
currentLyrics = track.metadata?.synchronized_lyrics || null;
const lyricsContent = document.getElementById('lyrics-content');
if (track.metadata?.lyrics || currentLyrics) {
if (currentLyrics) {
displaySyncedLyrics(currentLyrics);
} else {
lyricsContent.innerHTML = `
${track.metadata.lyrics || ''}
`;
}
} else {
lyricsContent.innerHTML = 'No lyrics available
';
}
}
function displaySyncedLyrics(lyrics) {
const lyricsContent = document.getElementById('lyrics-content');
lyricsContent.innerHTML = lyrics
.map((line, index) => `
${line.text}
`)
.join('');
// Add click handlers for each line
lyricsContent.querySelectorAll('.lyrics-line').forEach(line => {
line.addEventListener('click', () => {
const time = parseFloat(line.dataset.time);
if (!isNaN(time)) {
audioPlayer.currentTime = time;
audioPlayer.play()
.then(() => updatePlayButton(true))
.catch(error => console.error('Error playing:', error));
}
});
});
}
function updateActiveLyricLine(currentTime) {
if (!currentLyrics || !currentLyrics.length) return;
// Find the current line by looking for the last lyric before current time
let currentLineIndex = -1;
for (let i = 0; i < currentLyrics.length; i++) {
if (currentLyrics[i].time <= currentTime) {
currentLineIndex = i;
} else {
break;
}
}
// Remove active class from all lines
document.querySelectorAll('.lyrics-line').forEach(line => {
line.classList.remove('active');
});
// If we found a current line, highlight it and scroll to it
if (currentLineIndex >= 0) {
const lineElement = document.getElementById(`lyric-line-${currentLineIndex}`);
if (lineElement) {
lineElement.classList.add('active');
// Improved scrolling logic
const container = document.getElementById('lyrics-content');
const containerRect = container.getBoundingClientRect();
const lineRect = lineElement.getBoundingClientRect();
// Only scroll if the line is outside the visible area
if (lineRect.top < containerRect.top || lineRect.bottom > containerRect.bottom) {
lineElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
}
}
function updatePlayButton(playing) {
isPlaying = playing;
playBtn.innerHTML = playing ? '' : '';
}
function highlightCurrentTrack() {
document.querySelectorAll('.track-list .music-item').forEach((item, index) => {
item.classList.toggle('playing', index === currentTrack);
});
}
// Event Listeners
playBtn.onclick = () => {
if (currentTrack === null && playlist.length > 0) {
playTrack(0);
} else if (audioPlayer.paused) {
audioPlayer.play();
updatePlayButton(true);
} else {
audioPlayer.pause();
updatePlayButton(false);
}
};
prevBtn.onclick = () => {
playPrevious();
};
nextBtn.onclick = () => {
playNext();
};
volumeSlider.oninput = (e) => {
audioPlayer.volume = e.target.value / 100;
};
seekSlider.oninput = (e) => {
const time = (audioPlayer.duration * e.target.value) / 100;
audioPlayer.currentTime = time;
};
audioPlayer.ontimeupdate = () => {
const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100;
seekSlider.value = percent;
currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
updateActiveLyricLine(audioPlayer.currentTime);
};
audioPlayer.onloadedmetadata = () => {
durationSpan.textContent = formatTime(audioPlayer.duration);
};
audioPlayer.onended = () => {
playNext();
};
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
function initializeViews() {
document.querySelectorAll('.nav-menu a').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const view = e.target.dataset.view;
switchView(view);
});
});
document.getElementById('search-input').addEventListener('input', (e) => {
filterContent(e.target.value);
});
document.getElementById('new-playlist-btn').addEventListener('click', showPlaylistModal);
document.getElementById('save-playlist').addEventListener('click', savePlaylist);
document.getElementById('cancel-playlist').addEventListener('click', hidePlaylistModal);
renderPlaylists();
// Initialize with all songs view
renderAllSongs();
}
function switchView(view) {
currentView = view;
document.querySelectorAll('.nav-menu a').forEach(link => {
link.classList.toggle('active', link.dataset.view === view);
});
switch(view) {
case 'albums':
renderAlbumView();
break;
case 'artists':
renderArtistView();
break;
case 'playlists':
renderPlaylistView();
break;
default:
renderAllSongs();
}
}
// Optimize renderAlbumView
function renderAlbumView() {
if (shouldRebuildCaches()) {
ALBUM_CACHE.clear();
playlist.forEach(track => {
if (track.metadata) {
const albumName = track.metadata.album || 'Unknown Album';
const artistName = track.metadata.artist || 'Unknown Artist';
const albumKey = `${albumName}___${artistName}`;
if (!ALBUM_CACHE.has(albumKey)) {
ALBUM_CACHE.set(albumKey, {
name: albumName,
artist: artistName,
artwork: track.metadata.artwork || DEFAULT_COVER,
tracks: []
});
}
ALBUM_CACHE.get(albumKey).tracks.push(track);
}
});
}
const container = document.getElementById('view-container');
const albumsHtml = Array.from(ALBUM_CACHE.values())
.map(album => {
const escapedName = escapeHtml(album.name);
const escapedArtist = escapeHtml(album.artist);
return `
${escapedName}
${album.artist}
${album.tracks.length} tracks
`;
}).join('');
container.innerHTML = `${albumsHtml}
`;
}
// Optimize renderArtistView
function renderArtistView() {
if (shouldRebuildCaches()) {
ARTIST_CACHE.clear();
playlist.forEach(track => {
if (track.metadata) {
const artistName = track.metadata.artist || 'Unknown Artist';
if (!ARTIST_CACHE.has(artistName)) {
ARTIST_CACHE.set(artistName, {
name: artistName,
artwork: track.metadata.artwork || DEFAULT_COVER,
tracks: []
});
}
const artist = ARTIST_CACHE.get(artistName);
artist.tracks.push(track);
if (!artist.artwork && track.metadata.artwork) {
artist.artwork = track.metadata.artwork;
}
}
});
}
const container = document.getElementById('view-container');
const artistsHtml = Array.from(ARTIST_CACHE.values())
.map(artist => {
const escapedName = escapeHtml(artist.name);
return `
${escapedName}
${artist.tracks.length} tracks
`;
}).join('');
container.innerHTML = `${artistsHtml}
`;
}
// Optimize track filtering functions
function getAlbumTracks(albumName, artistName) {
const albumKey = `${albumName}___${artistName}`;
return ALBUM_CACHE.get(albumKey)?.tracks || [];
}
function getArtistTracks(artistName) {
return ARTIST_CACHE.get(artistName)?.tracks || [];
}
function showAlbumTracks(albumName, artistName) {
const tracks = getAlbumTracks(albumName, artistName);
const container = document.getElementById('view-container');
const viewTitle = document.getElementById('view-title');
viewTitle.textContent = `Album: ${albumName}`;
const albumArtwork = tracks[0]?.metadata?.artwork || DEFAULT_COVER;
container.innerHTML = `
${tracks.map((track, index) => `
${(index + 1).toString().padStart(2, '0')}
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown Artist'}
`).join('')}
`;
}
function showArtistTracks(artistName) {
const tracks = getArtistTracks(artistName);
renderTrackList(tracks, `Artist: ${artistName}`);
}
function renderTrackList(tracks, title) {
const container = document.getElementById('view-container');
document.getElementById('view-title').textContent = title;
container.innerHTML = `
${tracks.map((track, index) => `
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown'}
`).join('')}
`;
}
function showTrackMenu(event, trackIndex) {
event.stopPropagation();
// Remove any existing menus
document.querySelectorAll('.context-menu').forEach(m => m.remove());
const menu = document.createElement('div');
menu.className = 'context-menu';
const track = playlist[trackIndex];
menu.innerHTML = `
`;
document.body.appendChild(menu);
// Position the menu
const rect = event.target.closest('.menu-trigger').getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.left = `${rect.left - menu.offsetWidth + rect.width}px`;
menu.style.top = `${rect.bottom}px`;
// Close menu when clicking outside
document.addEventListener('click', function closeMenu(e) {
if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
});
}
function showPlaylistModal(trackIndex = null) {
const modal = document.getElementById('playlist-modal');
modal.dataset.trackIndex = trackIndex !== null ? trackIndex : '';
// Clear previous input
document.getElementById('playlist-name').value = '';
// Update modal title and render existing playlists
const modalTitle = modal.querySelector('h2');
modalTitle.textContent = trackIndex !== null ? 'Add to Playlist' : 'Create Playlist';
// Render existing playlists
const existingPlaylists = modal.querySelector('.existing-playlists');
if (trackIndex !== null) {
existingPlaylists.innerHTML = Object.values(playlists).map(playlist => `
${playlist.name}
${playlist.tracks.length} tracks
`).join('') || 'No playlists yet
';
} else {
existingPlaylists.innerHTML = '';
}
modal.style.display = 'flex';
}
function addToExistingPlaylist(playlistName, trackIndex) {
const track = playlist[trackIndex];
if (!track) return;
if (!playlists[playlistName].tracks.some(t => t.path === track.path)) {
playlists[playlistName].tracks.push(track);
localStorage.setItem('playlists', JSON.stringify(playlists));
showNotification(`Added to ${playlistName}`);
renderPlaylists();
// If we're in playlist view, refresh it
if (currentView === 'playlists') {
renderPlaylistView();
}
} else {
showNotification('Track already in playlist');
}
hidePlaylistModal();
}
function hidePlaylistModal() {
document.getElementById('playlist-modal').style.display = 'none';
document.getElementById('playlist-name').value = '';
}
function savePlaylist() {
const name = document.getElementById('playlist-name').value.trim();
const trackIndex = document.getElementById('playlist-modal').dataset.trackIndex;
if (!name) {
showNotification('Please enter a playlist name');
return;
}
if (playlists[name]) {
showNotification('Playlist already exists');
return;
}
// Create new playlist
playlists[name] = {
name: name,
tracks: []
};
// If we're adding a specific track
if (trackIndex !== '') {
const track = playlist[parseInt(trackIndex)];
playlists[name].tracks.push(track);
}
localStorage.setItem('playlists', JSON.stringify(playlists));
renderPlaylists();
hidePlaylistModal();
showNotification('Playlist created');
// If we're in playlist view, refresh it
if (currentView === 'playlists') {
renderPlaylistView();
}
}
function renderPlaylists() {
const container = document.getElementById('playlist-list');
container.innerHTML = Object.values(playlists).map(playlist => `
${playlist.name}
${playlist.tracks.length}
`).join('');
}
function showPlaylist(name) {
if (!playlists[name]) return;
currentPlaylist = playlists[name];
isPlayingPlaylist = true;
currentView = 'playlist-detail';
const container = document.getElementById('view-container');
const viewTitle = document.getElementById('view-title');
viewTitle.textContent = name;
container.innerHTML = `
${currentPlaylist.tracks.map((track, index) => `
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown'}
`).join('')}
`;
}
function playPlaylist(name) {
if (!playlists[name] || !playlists[name].tracks.length) return;
currentPlaylist = playlists[name];
isPlayingPlaylist = true;
// Find the first track in the main playlist
const firstTrack = currentPlaylist.tracks[0];
const mainPlaylistIndex = playlist.findIndex(t => t.path === firstTrack.path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
showNotification(`Playing playlist: ${name}`);
}
}
function playPlaylistTrack(playlistName, index) {
if (!playlists[playlistName]) return;
currentPlaylist = playlists[playlistName];
isPlayingPlaylist = true;
const track = currentPlaylist.tracks[index];
// Find the track in the main playlist
const mainPlaylistIndex = playlist.findIndex(t => t.path === track.path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
}
}
function showPlaylistTrackMenu(event, playlistName, index) {
event.stopPropagation();
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.innerHTML = `
`;
// Remove any existing menus
document.querySelectorAll('.context-menu').forEach(m => m.remove());
document.body.appendChild(menu);
// Position the menu
const rect = event.target.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.left = `${rect.left}px`;
menu.style.top = `${rect.bottom}px`;
// Close menu when clicking outside
document.addEventListener('click', function closeMenu(e) {
if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
});
}
function playNext() {
if (isPlayingPlaylist && currentPlaylist) {
// Find current track index in playlist
const currentTrackPath = playlist[currentTrack].path;
const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath);
if (playlistTrackIndex < currentPlaylist.tracks.length - 1) {
// Play next track in playlist
const nextTrack = currentPlaylist.tracks[playlistTrackIndex + 1];
const mainPlaylistIndex = playlist.findIndex(t => t.path === nextTrack.path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
return;
}
} else {
// End of playlist reached
currentPlaylist = null;
isPlayingPlaylist = false;
updatePlayButton(false);
return;
}
}
// If no playlist or playlist ended, play next in main playlist
if (currentTrack < playlist.length - 1) {
playTrack(currentTrack + 1);
} else {
updatePlayButton(false);
}
}
// Add this function to handle previous track
function playPrevious() {
if (isPlayingPlaylist && currentPlaylist) {
// Find current track index in playlist
const currentTrackPath = playlist[currentTrack].path;
const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath);
if (playlistTrackIndex > 0) {
// Play previous track in playlist
const prevTrack = currentPlaylist.tracks[playlistTrackIndex - 1];
const mainPlaylistIndex = playlist.findIndex(t => t.path === prevTrack.path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
return;
}
}
}
// If no playlist or at start of playlist, play previous in main playlist
if (currentTrack > 0) {
playTrack(currentTrack - 1);
}
}
// Add this function to initialize audio player event listeners
function initializeAudioPlayer() {
audioPlayer.addEventListener('ended', () => {
playNext();
});
audioPlayer.addEventListener('error', (e) => {
console.error('Audio player error:', e);
showNotification('Error playing track');
playNext(); // Skip to next track on error
});
// Add this timeupdate listener here instead of in DOMContentLoaded
audioPlayer.addEventListener('timeupdate', () => {
const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100;
seekSlider.value = percent;
currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
updateActiveLyricLine(audioPlayer.currentTime);
});
}
// Update the initialization code
document.addEventListener('DOMContentLoaded', function() {
initializeAudioPlayer();
document.querySelectorAll('.queue-item-actions button').forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
});
});
// Add lyrics toggle handler
document.getElementById('lyrics-toggle').addEventListener('click', () => {
syncedLyricsMode = !syncedLyricsMode;
if (currentTrack !== null) {
updatePlayerInfo(playlist[currentTrack]);
}
});
});
function renderAllSongs() {
const container = document.getElementById('view-container');
const viewTitle = document.getElementById('view-title');
viewTitle.textContent = 'All Songs';
container.innerHTML = `
${playlist.map((track, index) => `
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown'}
`).join('')}
`;
}
function filterContent(query) {
query = query.toLowerCase();
const filteredTracks = playlist.filter(track =>
(track.metadata?.title || track.name).toLowerCase().includes(query) ||
(track.metadata?.artist || 'Unknown').toLowerCase().includes(query) ||
(track.metadata?.album || 'Unknown').toLowerCase().includes(query)
);
const container = document.getElementById('view-container');
const viewTitle = document.getElementById('view-title');
viewTitle.textContent = 'Search Results';
container.innerHTML = `
${filteredTracks.map((track, index) => `
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown'}
`).join('')}
`;
}
function renderPlaylistView() {
const container = document.getElementById('view-container');
const viewTitle = document.getElementById('view-title');
viewTitle.textContent = 'Playlists';
container.innerHTML = `
${Object.values(playlists).map(playlist => `
${playlist.name}
${playlist.tracks.length} tracks
`).join('')}
`;
}
function deletePlaylist(event, playlistName) {
event.stopPropagation();
if (confirm(`Delete playlist "${playlistName}"?`)) {
delete playlists[playlistName];
localStorage.setItem('playlists', JSON.stringify(playlists));
renderPlaylistView();
renderPlaylists();
showNotification('Playlist deleted');
}
}
function removeFromPlaylist(playlistName, index) {
if (!playlists[playlistName]) return;
playlists[playlistName].tracks.splice(index, 1);
localStorage.setItem('playlists', JSON.stringify(playlists));
// Refresh the current playlist view
showPlaylist(playlistName);
renderPlaylists();
showNotification('Track removed from playlist');
}
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 300);
}, 2000);
}
// Add these new functions to handle playing albums and artists
function playAlbum(albumName, artistName) {
const albumTracks = getAlbumTracks(albumName, artistName);
if (albumTracks.length > 0) {
currentPlaylist = {
name: albumName,
tracks: [...albumTracks]
};
isPlayingPlaylist = true;
const mainPlaylistIndex = playlist.findIndex(track => track.path === albumTracks[0].path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
showNotification(`Playing album: ${albumName}`);
}
}
}
function playArtist(artistName) {
const tracks = getArtistTracks(artistName);
if (tracks.length > 0) {
currentPlaylist = {
name: artistName,
tracks: [...tracks]
};
isPlayingPlaylist = true;
const mainPlaylistIndex = playlist.findIndex(track => track.path === tracks[0].path);
if (mainPlaylistIndex !== -1) {
playTrack(mainPlaylistIndex);
showNotification(`Playing artist: ${artistName}`);
}
}
}
// Add this new function to handle playing from main playlist
function playFromMainPlaylist(index) {
isPlayingPlaylist = false;
currentPlaylist = null;
playTrack(index);
}
// Add this helper function to escape HTML special characters
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}