AdityaAdaki commited on
Commit
f988d03
·
1 Parent(s): dd81642

docker update

Browse files
Files changed (7) hide show
  1. .dockerignore +0 -14
  2. Dockerfile +12 -13
  3. README.md +17 -9
  4. app.py +0 -9
  5. docker-compose.yml +4 -4
  6. space.yml +6 -0
  7. static/player.js +857 -29
.dockerignore DELETED
@@ -1,14 +0,0 @@
1
- __pycache__
2
- *.pyc
3
- *.pyo
4
- *.pyd
5
- .Python
6
- env/
7
- venv/
8
- .env
9
- *.git
10
- .gitignore
11
- .dockerignore
12
- Dockerfile
13
- *.md
14
- music/*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,31 +1,30 @@
1
  FROM python:3.9-slim
2
 
3
- # Set working directory
4
- WORKDIR /app
5
 
6
- # Install system dependencies for mutagen
7
  RUN apt-get update && apt-get install -y --no-install-recommends \
8
- gcc \
9
- libc6-dev \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
- # Copy requirements first for better caching
13
  COPY requirements.txt .
14
  RUN pip install --no-cache-dir -r requirements.txt
15
 
16
  # Copy application code
17
  COPY . .
18
 
19
- # Create music directory with proper permissions
20
- RUN mkdir -p music && chmod 777 music
21
 
22
  # Set environment variables
23
- ENV FLASK_APP=app.py
24
- ENV FLASK_ENV=production
25
  ENV PORT=7860
 
 
 
 
 
26
 
27
- # Expose port
28
  EXPOSE 7860
29
 
30
- # Run with gunicorn for better performance
31
- CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--threads", "4", "app:app"]
 
1
  FROM python:3.9-slim
2
 
3
+ WORKDIR /code
 
4
 
5
+ # Install system dependencies
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ ffmpeg \
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ # Copy requirements first to leverage Docker cache
11
  COPY requirements.txt .
12
  RUN pip install --no-cache-dir -r requirements.txt
13
 
14
  # Copy application code
15
  COPY . .
16
 
17
+ # Create necessary directories
18
+ RUN mkdir -p /code/music /code/static /code/templates
19
 
20
  # Set environment variables
 
 
21
  ENV PORT=7860
22
+ ENV PYTHONUNBUFFERED=1
23
+ ENV FLASK_ENV=production
24
+
25
+ # Set permissions
26
+ RUN chmod -R 755 /code
27
 
 
28
  EXPOSE 7860
29
 
30
+ CMD ["python", "app.py"]
 
README.md CHANGED
@@ -7,18 +7,26 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # Music Player Web App
11
 
12
- A Flask-based web application for playing music files with metadata support. Supports MP3, WAV, and FLAC formats.
13
 
14
  ## Features
15
- - Music file browsing
16
- - Metadata display
17
- - Playlist management
18
- - Lyrics support
19
- - Album artwork display
 
 
20
 
21
  ## Usage
22
- 1. Upload your music files to the 'music' directory
 
23
  2. Access the web interface
24
- 3. Browse and play your music collection
 
 
 
 
 
 
7
  pinned: false
8
  ---
9
 
10
+ # 🎵 Music Player
11
 
12
+ A web-based music player application that supports MP3, FLAC, and WAV files with metadata handling and playlist management.
13
 
14
  ## Features
15
+
16
+ - 🎧 Supports MP3, FLAC, and WAV audio formats
17
+ - 📝 Displays track metadata (title, artist, album)
18
+ - 🖼️ Shows album artwork when available
19
+ - 📜 Supports lyrics display (including synchronized lyrics)
20
+ - 📋 Playlist management
21
+ - 🎨 Clean, modern interface
22
 
23
  ## Usage
24
+
25
+ 1. Upload your music files to the `music` directory
26
  2. Access the web interface
27
+ 3. Browse and play your music collection
28
+ 4. Create and manage playlists
29
+
30
+ ## Development
31
+
32
+ To run locally:
app.py CHANGED
@@ -295,15 +295,6 @@ def parse_sylt_format(sylt):
295
  })
296
  return sorted(lyrics, key=lambda x: x['time'])
297
 
298
- @app.route('/health')
299
- def health_check():
300
- """Health check endpoint for container orchestration"""
301
- return jsonify({
302
- "status": "healthy",
303
- "cache_version": CACHE_VERSION,
304
- "cache_size": len(METADATA_CACHE)
305
- })
306
-
307
  if __name__ == '__main__':
308
  # Create music directory if it doesn't exist
309
  if not os.path.exists(app.config['MUSIC_FOLDER']):
 
295
  })
296
  return sorted(lyrics, key=lambda x: x['time'])
297
 
 
 
 
 
 
 
 
 
 
298
  if __name__ == '__main__':
299
  # Create music directory if it doesn't exist
300
  if not os.path.exists(app.config['MUSIC_FOLDER']):
docker-compose.yml CHANGED
@@ -6,13 +6,13 @@ services:
6
  ports:
7
  - "7860:7860"
8
  volumes:
9
- - ./music:/app/music
10
  environment:
11
- - FLASK_ENV=development
12
- - FLASK_APP=app.py
13
  - PORT=7860
 
 
14
  healthcheck:
15
- test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
16
  interval: 30s
17
  timeout: 10s
18
  retries: 3
 
6
  ports:
7
  - "7860:7860"
8
  volumes:
9
+ - ./music:/code/music
10
  environment:
 
 
11
  - PORT=7860
12
+ - FLASK_ENV=production
13
+ restart: unless-stopped
14
  healthcheck:
15
+ test: ["CMD", "curl", "-f", "http://localhost:7860/"]
16
  interval: 30s
17
  timeout: 10s
18
  retries: 3
space.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ title: Music Player
2
+ emoji: 🎵
3
+ colorFrom: blue
4
+ colorTo: indigo
5
+ sdk: docker
6
+ app_port: 7860
static/player.js CHANGED
@@ -19,40 +19,12 @@ const durationSpan = document.getElementById('duration');
19
  // Add this constant at the top of the file
20
  const DEFAULT_COVER = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg==';
21
 
22
- // Add near the top with other constants
23
- const CACHE_VERSION = 1; // Should match app.py CACHE_VERSION
24
-
25
- // Add this function
26
- async function clearCache() {
27
- try {
28
- const response = await fetch('/api/clear-cache', {
29
- method: 'POST'
30
- });
31
- if (response.ok) {
32
- console.log('Cache cleared successfully');
33
- // Reload music structure
34
- fetch('/api/music-structure')
35
- .then(response => response.json())
36
- .then(data => {
37
- playlist = flattenPlaylist(data);
38
- initializeViews();
39
- });
40
- }
41
- } catch (error) {
42
- console.error('Error clearing cache:', error);
43
- }
44
- }
45
-
46
  // Fetch music structure from the server
47
  fetch('/api/music-structure')
48
  .then(response => response.json())
49
  .then(data => {
50
  playlist = flattenPlaylist(data);
51
  initializeViews();
52
- })
53
- .catch(error => {
54
- console.error('Error loading music structure:', error);
55
- showNotification('Error loading music library');
56
  });
57
 
58
  function flattenPlaylist(structure, prefix = '') {
@@ -68,4 +40,860 @@ function flattenPlaylist(structure, prefix = '') {
68
  return flat;
69
  }
70
 
71
- [... rest of the original file remains exactly the same ...]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  // Add this constant at the top of the file
20
  const DEFAULT_COVER = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg==';
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  // Fetch music structure from the server
23
  fetch('/api/music-structure')
24
  .then(response => response.json())
25
  .then(data => {
26
  playlist = flattenPlaylist(data);
27
  initializeViews();
 
 
 
 
28
  });
29
 
30
  function flattenPlaylist(structure, prefix = '') {
 
40
  return flat;
41
  }
42
 
43
+ function playTrack(index) {
44
+ // If playing directly from main playlist, clear playlist context
45
+ if (!isPlayingPlaylist) {
46
+ currentPlaylist = null;
47
+ }
48
+
49
+ currentTrack = index;
50
+ const track = playlist[index];
51
+
52
+ if (!track) return;
53
+
54
+ audioPlayer.src = `/music/${track.path}`;
55
+ audioPlayer.play()
56
+ .then(() => {
57
+ updatePlayerInfo(track);
58
+ updatePlayButton(true);
59
+ highlightCurrentTrack();
60
+ updateActiveLyricLine(audioPlayer.currentTime);
61
+ })
62
+ .catch(error => {
63
+ console.error('Error playing track:', error);
64
+ showNotification('Error playing track');
65
+ playNext();
66
+ });
67
+ }
68
+
69
+ function updatePlayerInfo(track) {
70
+ document.getElementById('track-title').textContent = track.metadata?.title || track.name;
71
+ document.getElementById('track-artist').textContent = track.metadata?.artist || 'Unknown';
72
+ document.getElementById('track-album').textContent = track.metadata?.album || 'Unknown';
73
+
74
+ // Update album art with error handling
75
+ const albumArt = document.getElementById('album-art');
76
+ if (track.metadata?.artwork) {
77
+ albumArt.src = track.metadata.artwork;
78
+ // Add error handling for the album art
79
+ albumArt.onerror = () => {
80
+ albumArt.src = DEFAULT_COVER;
81
+ };
82
+ } else {
83
+ albumArt.src = DEFAULT_COVER;
84
+ }
85
+
86
+ // Update lyrics
87
+ currentLyrics = track.metadata?.synchronized_lyrics || null;
88
+ const lyricsContent = document.getElementById('lyrics-content');
89
+
90
+ if (track.metadata?.lyrics || currentLyrics) {
91
+ if (currentLyrics) {
92
+ displaySyncedLyrics(currentLyrics);
93
+ } else {
94
+ lyricsContent.innerHTML = `<div class="lyrics-line">${track.metadata.lyrics || ''}</div>`;
95
+ }
96
+ } else {
97
+ lyricsContent.innerHTML = '<div class="lyrics-line">No lyrics available</div>';
98
+ }
99
+ }
100
+
101
+ function displaySyncedLyrics(lyrics) {
102
+ const lyricsContent = document.getElementById('lyrics-content');
103
+ lyricsContent.innerHTML = lyrics
104
+ .map((line, index) => `
105
+ <div class="lyrics-line" data-time="${line.time}" id="lyric-line-${index}">
106
+ ${line.text}
107
+ </div>
108
+ `)
109
+ .join('');
110
+
111
+ // Add click handlers for each line
112
+ lyricsContent.querySelectorAll('.lyrics-line').forEach(line => {
113
+ line.addEventListener('click', () => {
114
+ const time = parseFloat(line.dataset.time);
115
+ if (!isNaN(time)) {
116
+ audioPlayer.currentTime = time;
117
+ audioPlayer.play()
118
+ .then(() => updatePlayButton(true))
119
+ .catch(error => console.error('Error playing:', error));
120
+ }
121
+ });
122
+ });
123
+ }
124
+
125
+ function updateActiveLyricLine(currentTime) {
126
+ if (!currentLyrics || !currentLyrics.length) return;
127
+
128
+ // Find the current line by looking for the last lyric before current time
129
+ let currentLineIndex = -1;
130
+ for (let i = 0; i < currentLyrics.length; i++) {
131
+ if (currentLyrics[i].time <= currentTime) {
132
+ currentLineIndex = i;
133
+ } else {
134
+ break;
135
+ }
136
+ }
137
+
138
+ // Remove active class from all lines
139
+ document.querySelectorAll('.lyrics-line').forEach(line => {
140
+ line.classList.remove('active');
141
+ });
142
+
143
+ // If we found a current line, highlight it and scroll to it
144
+ if (currentLineIndex >= 0) {
145
+ const lineElement = document.getElementById(`lyric-line-${currentLineIndex}`);
146
+ if (lineElement) {
147
+ lineElement.classList.add('active');
148
+
149
+ // Improved scrolling logic
150
+ const container = document.getElementById('lyrics-content');
151
+ const containerRect = container.getBoundingClientRect();
152
+ const lineRect = lineElement.getBoundingClientRect();
153
+
154
+ // Only scroll if the line is outside the visible area
155
+ if (lineRect.top < containerRect.top || lineRect.bottom > containerRect.bottom) {
156
+ lineElement.scrollIntoView({
157
+ behavior: 'smooth',
158
+ block: 'center'
159
+ });
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ function updatePlayButton(playing) {
166
+ isPlaying = playing;
167
+ playBtn.innerHTML = playing ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
168
+ }
169
+
170
+ function highlightCurrentTrack() {
171
+ document.querySelectorAll('.track-list .music-item').forEach((item, index) => {
172
+ item.classList.toggle('playing', index === currentTrack);
173
+ });
174
+ }
175
+
176
+ // Event Listeners
177
+ playBtn.onclick = () => {
178
+ if (currentTrack === null && playlist.length > 0) {
179
+ playTrack(0);
180
+ } else if (audioPlayer.paused) {
181
+ audioPlayer.play();
182
+ updatePlayButton(true);
183
+ } else {
184
+ audioPlayer.pause();
185
+ updatePlayButton(false);
186
+ }
187
+ };
188
+
189
+ prevBtn.onclick = () => {
190
+ playPrevious();
191
+ };
192
+
193
+ nextBtn.onclick = () => {
194
+ playNext();
195
+ };
196
+
197
+ volumeSlider.oninput = (e) => {
198
+ audioPlayer.volume = e.target.value / 100;
199
+ };
200
+
201
+ seekSlider.oninput = (e) => {
202
+ const time = (audioPlayer.duration * e.target.value) / 100;
203
+ audioPlayer.currentTime = time;
204
+ };
205
+
206
+ audioPlayer.ontimeupdate = () => {
207
+ const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100;
208
+ seekSlider.value = percent;
209
+ currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
210
+ updateActiveLyricLine(audioPlayer.currentTime);
211
+ };
212
+
213
+ audioPlayer.onloadedmetadata = () => {
214
+ durationSpan.textContent = formatTime(audioPlayer.duration);
215
+ };
216
+
217
+ audioPlayer.onended = () => {
218
+ playNext();
219
+ };
220
+
221
+ function formatTime(seconds) {
222
+ const minutes = Math.floor(seconds / 60);
223
+ const remainingSeconds = Math.floor(seconds % 60);
224
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
225
+ }
226
+
227
+ function initializeViews() {
228
+ document.querySelectorAll('.nav-menu a').forEach(link => {
229
+ link.addEventListener('click', (e) => {
230
+ e.preventDefault();
231
+ const view = e.target.dataset.view;
232
+ switchView(view);
233
+ });
234
+ });
235
+
236
+ document.getElementById('search-input').addEventListener('input', (e) => {
237
+ filterContent(e.target.value);
238
+ });
239
+
240
+ document.getElementById('new-playlist-btn').addEventListener('click', showPlaylistModal);
241
+ document.getElementById('save-playlist').addEventListener('click', savePlaylist);
242
+ document.getElementById('cancel-playlist').addEventListener('click', hidePlaylistModal);
243
+
244
+ renderPlaylists();
245
+
246
+ // Initialize with all songs view
247
+ renderAllSongs();
248
+ }
249
+
250
+ function switchView(view) {
251
+ currentView = view;
252
+ document.querySelectorAll('.nav-menu a').forEach(link => {
253
+ link.classList.toggle('active', link.dataset.view === view);
254
+ });
255
+
256
+ switch(view) {
257
+ case 'albums':
258
+ renderAlbumView();
259
+ break;
260
+ case 'artists':
261
+ renderArtistView();
262
+ break;
263
+ case 'playlists':
264
+ renderPlaylistView();
265
+ break;
266
+ default:
267
+ renderAllSongs();
268
+ }
269
+ }
270
+
271
+ function renderAlbumView() {
272
+ const albums = {};
273
+ playlist.forEach(track => {
274
+ if (track.metadata) {
275
+ const albumName = track.metadata.album || 'Unknown Album';
276
+ if (!albums[albumName]) {
277
+ albums[albumName] = {
278
+ name: albumName,
279
+ artwork: track.metadata.artwork || DEFAULT_COVER,
280
+ tracks: []
281
+ };
282
+ }
283
+ albums[albumName].tracks.push(track);
284
+ }
285
+ });
286
+
287
+ const container = document.getElementById('view-container');
288
+ container.innerHTML = `
289
+ <div class="album-grid">
290
+ ${Object.values(albums).map(album => `
291
+ <div class="album-card">
292
+ <div class="album-art-container">
293
+ <img class="album-art" src="${album.artwork}" alt="${album.name}">
294
+ <button class="play-overlay" onclick="playAlbum('${album.name}')">
295
+ <i class="fas fa-play"></i>
296
+ </button>
297
+ </div>
298
+ <div class="album-info" onclick="showAlbumTracks('${album.name}')">
299
+ <h3>${album.name}</h3>
300
+ <p>${album.tracks.length} tracks</p>
301
+ </div>
302
+ </div>
303
+ `).join('')}
304
+ </div>
305
+ `;
306
+ }
307
+
308
+ function renderArtistView() {
309
+ const artists = {};
310
+ playlist.forEach(track => {
311
+ if (track.metadata) {
312
+ const artistName = track.metadata.artist || 'Unknown Artist';
313
+ if (!artists[artistName]) {
314
+ artists[artistName] = {
315
+ name: artistName,
316
+ artwork: track.metadata.artwork || DEFAULT_COVER,
317
+ tracks: []
318
+ };
319
+ }
320
+ artists[artistName].tracks.push(track);
321
+ // Use the first track's artwork as artist image if we don't have it yet
322
+ if (!artists[artistName].artwork && track.metadata.artwork) {
323
+ artists[artistName].artwork = track.metadata.artwork;
324
+ }
325
+ }
326
+ });
327
+
328
+ const container = document.getElementById('view-container');
329
+ container.innerHTML = `
330
+ <div class="artist-grid">
331
+ ${Object.values(artists).map(artist => `
332
+ <div class="artist-card">
333
+ <div class="artist-art-container">
334
+ <img class="artist-image" src="${artist.artwork}" alt="${artist.name}">
335
+ <button class="play-overlay" onclick="playArtist('${artist.name}')">
336
+ <i class="fas fa-play"></i>
337
+ </button>
338
+ </div>
339
+ <div class="artist-info" onclick="showArtistTracks('${artist.name}')">
340
+ <h3>${artist.name}</h3>
341
+ <p>${artist.tracks.length} tracks</p>
342
+ </div>
343
+ </div>
344
+ `).join('')}
345
+ </div>
346
+ `;
347
+ }
348
+
349
+ function showAlbumTracks(albumName) {
350
+ const tracks = playlist.filter(track =>
351
+ track.metadata?.album === albumName
352
+ );
353
+ renderTrackList(tracks, `Album: ${albumName}`);
354
+ }
355
+
356
+ function showArtistTracks(artistName) {
357
+ const tracks = playlist.filter(track =>
358
+ track.metadata?.artist === artistName
359
+ );
360
+ renderTrackList(tracks, `Artist: ${artistName}`);
361
+ }
362
+
363
+ function renderTrackList(tracks, title) {
364
+ const container = document.getElementById('view-container');
365
+ document.getElementById('view-title').textContent = title;
366
+
367
+ container.innerHTML = `
368
+ <div class="track-list">
369
+ ${tracks.map((track, index) => `
370
+ <div class="music-item">
371
+ <div class="track-info" onclick="playTrack(${playlist.indexOf(track)})">
372
+ <div class="track-name">${track.metadata?.title || track.name}</div>
373
+ <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div>
374
+ </div>
375
+ <div class="track-actions">
376
+ <button class="menu-trigger" onclick="showTrackMenu(event, ${playlist.indexOf(track)})">
377
+ <i class="fas fa-ellipsis-v"></i>
378
+ </button>
379
+ </div>
380
+ </div>
381
+ `).join('')}
382
+ </div>
383
+ `;
384
+ }
385
+
386
+ function showTrackMenu(event, trackIndex) {
387
+ event.stopPropagation();
388
+
389
+ // Remove any existing menus
390
+ document.querySelectorAll('.context-menu').forEach(m => m.remove());
391
+
392
+ const menu = document.createElement('div');
393
+ menu.className = 'context-menu';
394
+
395
+ const track = playlist[trackIndex];
396
+ menu.innerHTML = `
397
+ <div class="menu-items">
398
+ <div class="menu-item" onclick="playTrack(${trackIndex})">
399
+ <i class="fas fa-play"></i> Play
400
+ </div>
401
+ <div class="menu-item" onclick="showPlaylistModal(${trackIndex})">
402
+ <i class="fas fa-plus"></i> Add to Playlist
403
+ </div>
404
+ ${currentPlaylist ? `
405
+ <div class="menu-item" onclick="removeFromPlaylist('${currentPlaylist.name}', ${trackIndex})">
406
+ <i class="fas fa-trash"></i> Remove from Playlist
407
+ </div>
408
+ ` : ''}
409
+ </div>
410
+ `;
411
+
412
+ document.body.appendChild(menu);
413
+
414
+ // Position the menu
415
+ const rect = event.target.closest('.menu-trigger').getBoundingClientRect();
416
+ menu.style.position = 'fixed';
417
+ menu.style.left = `${rect.left - menu.offsetWidth + rect.width}px`;
418
+ menu.style.top = `${rect.bottom}px`;
419
+
420
+ // Close menu when clicking outside
421
+ document.addEventListener('click', function closeMenu(e) {
422
+ if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) {
423
+ menu.remove();
424
+ document.removeEventListener('click', closeMenu);
425
+ }
426
+ });
427
+ }
428
+
429
+ function showPlaylistModal(trackIndex = null) {
430
+ const modal = document.getElementById('playlist-modal');
431
+ modal.dataset.trackIndex = trackIndex !== null ? trackIndex : '';
432
+
433
+ // Clear previous input
434
+ document.getElementById('playlist-name').value = '';
435
+
436
+ // Update modal title and render existing playlists
437
+ const modalTitle = modal.querySelector('h2');
438
+ modalTitle.textContent = trackIndex !== null ? 'Add to Playlist' : 'Create Playlist';
439
+
440
+ // Render existing playlists
441
+ const existingPlaylists = modal.querySelector('.existing-playlists');
442
+ if (trackIndex !== null) {
443
+ existingPlaylists.innerHTML = Object.values(playlists).map(playlist => `
444
+ <div class="existing-playlist-item" onclick="addToExistingPlaylist('${playlist.name}', ${trackIndex})">
445
+ <i class="fas fa-list"></i>
446
+ ${playlist.name}
447
+ <span class="playlist-count">${playlist.tracks.length} tracks</span>
448
+ </div>
449
+ `).join('') || '<div class="no-playlists">No playlists yet</div>';
450
+ } else {
451
+ existingPlaylists.innerHTML = '';
452
+ }
453
+
454
+ modal.style.display = 'flex';
455
+ }
456
+
457
+ function addToExistingPlaylist(playlistName, trackIndex) {
458
+ const track = playlist[trackIndex];
459
+ if (!track) return;
460
+
461
+ if (!playlists[playlistName].tracks.some(t => t.path === track.path)) {
462
+ playlists[playlistName].tracks.push(track);
463
+ localStorage.setItem('playlists', JSON.stringify(playlists));
464
+ showNotification(`Added to ${playlistName}`);
465
+ renderPlaylists();
466
+
467
+ // If we're in playlist view, refresh it
468
+ if (currentView === 'playlists') {
469
+ renderPlaylistView();
470
+ }
471
+ } else {
472
+ showNotification('Track already in playlist');
473
+ }
474
+
475
+ hidePlaylistModal();
476
+ }
477
+
478
+ function hidePlaylistModal() {
479
+ document.getElementById('playlist-modal').style.display = 'none';
480
+ document.getElementById('playlist-name').value = '';
481
+ }
482
+
483
+ function savePlaylist() {
484
+ const name = document.getElementById('playlist-name').value.trim();
485
+ const trackIndex = document.getElementById('playlist-modal').dataset.trackIndex;
486
+
487
+ if (!name) {
488
+ showNotification('Please enter a playlist name');
489
+ return;
490
+ }
491
+
492
+ if (playlists[name]) {
493
+ showNotification('Playlist already exists');
494
+ return;
495
+ }
496
+
497
+ // Create new playlist
498
+ playlists[name] = {
499
+ name: name,
500
+ tracks: []
501
+ };
502
+
503
+ // If we're adding a specific track
504
+ if (trackIndex !== '') {
505
+ const track = playlist[parseInt(trackIndex)];
506
+ playlists[name].tracks.push(track);
507
+ }
508
+
509
+ localStorage.setItem('playlists', JSON.stringify(playlists));
510
+ renderPlaylists();
511
+ hidePlaylistModal();
512
+
513
+ showNotification('Playlist created');
514
+
515
+ // If we're in playlist view, refresh it
516
+ if (currentView === 'playlists') {
517
+ renderPlaylistView();
518
+ }
519
+ }
520
+
521
+ function renderPlaylists() {
522
+ const container = document.getElementById('playlist-list');
523
+ container.innerHTML = Object.values(playlists).map(playlist => `
524
+ <li>
525
+ <a href="#" onclick="showPlaylist('${playlist.name}')">
526
+ <i class="fas fa-list"></i> ${playlist.name}
527
+ <span class="playlist-count">${playlist.tracks.length}</span>
528
+ </a>
529
+ </li>
530
+ `).join('');
531
+ }
532
+
533
+ function showPlaylist(name) {
534
+ if (!playlists[name]) return;
535
+
536
+ currentPlaylist = playlists[name];
537
+ isPlayingPlaylist = true;
538
+ currentView = 'playlist-detail';
539
+
540
+ const container = document.getElementById('view-container');
541
+ const viewTitle = document.getElementById('view-title');
542
+
543
+ viewTitle.textContent = name;
544
+
545
+ container.innerHTML = `
546
+ <div class="playlist-header">
547
+ <div class="playlist-info">
548
+ <i class="fas fa-list playlist-icon"></i>
549
+ <div>
550
+ <h2>${name}</h2>
551
+ <p>${currentPlaylist.tracks.length} tracks</p>
552
+ </div>
553
+ </div>
554
+ <button class="play-playlist" onclick="playPlaylist('${name}')">
555
+ <i class="fas fa-play"></i> Play
556
+ </button>
557
+ </div>
558
+ <div class="track-list">
559
+ ${currentPlaylist.tracks.map((track, index) => `
560
+ <div class="music-item" data-index="${index}">
561
+ <div class="track-info" onclick="playPlaylistTrack('${name}', ${index})">
562
+ <div class="track-name">${track.metadata?.title || track.name}</div>
563
+ <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div>
564
+ </div>
565
+ <div class="track-actions">
566
+ <button class="menu-trigger" onclick="showPlaylistTrackMenu(event, '${name}', ${index})">
567
+ <i class="fas fa-ellipsis-v"></i>
568
+ </button>
569
+ </div>
570
+ </div>
571
+ `).join('')}
572
+ </div>
573
+ `;
574
+ }
575
+
576
+ function playPlaylist(name) {
577
+ if (!playlists[name] || !playlists[name].tracks.length) return;
578
+
579
+ currentPlaylist = playlists[name];
580
+ isPlayingPlaylist = true;
581
+
582
+ // Find the first track in the main playlist
583
+ const firstTrack = currentPlaylist.tracks[0];
584
+ const mainPlaylistIndex = playlist.findIndex(t => t.path === firstTrack.path);
585
+
586
+ if (mainPlaylistIndex !== -1) {
587
+ playTrack(mainPlaylistIndex);
588
+ showNotification(`Playing playlist: ${name}`);
589
+ }
590
+ }
591
+
592
+ function playPlaylistTrack(playlistName, index) {
593
+ if (!playlists[playlistName]) return;
594
+
595
+ currentPlaylist = playlists[playlistName];
596
+ isPlayingPlaylist = true;
597
+ const track = currentPlaylist.tracks[index];
598
+
599
+ // Find the track in the main playlist
600
+ const mainPlaylistIndex = playlist.findIndex(t => t.path === track.path);
601
+ if (mainPlaylistIndex !== -1) {
602
+ playTrack(mainPlaylistIndex);
603
+ }
604
+ }
605
+
606
+ function showPlaylistTrackMenu(event, playlistName, index) {
607
+ event.stopPropagation();
608
+
609
+ const menu = document.createElement('div');
610
+ menu.className = 'context-menu';
611
+
612
+ menu.innerHTML = `
613
+ <div class="menu-items">
614
+ <div class="menu-item" onclick="playPlaylistTrack('${playlistName}', ${index})">
615
+ <i class="fas fa-play"></i> Play
616
+ </div>
617
+ <div class="menu-item" onclick="removeFromPlaylist('${playlistName}', ${index})">
618
+ <i class="fas fa-trash"></i> Remove from Playlist
619
+ </div>
620
+ </div>
621
+ `;
622
+
623
+ // Remove any existing menus
624
+ document.querySelectorAll('.context-menu').forEach(m => m.remove());
625
+
626
+ document.body.appendChild(menu);
627
+
628
+ // Position the menu
629
+ const rect = event.target.getBoundingClientRect();
630
+ menu.style.position = 'fixed';
631
+ menu.style.left = `${rect.left}px`;
632
+ menu.style.top = `${rect.bottom}px`;
633
+
634
+ // Close menu when clicking outside
635
+ document.addEventListener('click', function closeMenu(e) {
636
+ if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) {
637
+ menu.remove();
638
+ document.removeEventListener('click', closeMenu);
639
+ }
640
+ });
641
+ }
642
+
643
+ function playNext() {
644
+ if (isPlayingPlaylist && currentPlaylist) {
645
+ // Find current track index in playlist
646
+ const currentTrackPath = playlist[currentTrack].path;
647
+ const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath);
648
+
649
+ if (playlistTrackIndex < currentPlaylist.tracks.length - 1) {
650
+ // Play next track in playlist
651
+ const nextTrack = currentPlaylist.tracks[playlistTrackIndex + 1];
652
+ const mainPlaylistIndex = playlist.findIndex(t => t.path === nextTrack.path);
653
+ if (mainPlaylistIndex !== -1) {
654
+ playTrack(mainPlaylistIndex);
655
+ return;
656
+ }
657
+ } else {
658
+ // End of playlist reached
659
+ currentPlaylist = null;
660
+ isPlayingPlaylist = false;
661
+ updatePlayButton(false);
662
+ return;
663
+ }
664
+ }
665
+
666
+ // If no playlist or playlist ended, play next in main playlist
667
+ if (currentTrack < playlist.length - 1) {
668
+ playTrack(currentTrack + 1);
669
+ } else {
670
+ updatePlayButton(false);
671
+ }
672
+ }
673
+
674
+ // Add this function to handle previous track
675
+ function playPrevious() {
676
+ if (isPlayingPlaylist && currentPlaylist) {
677
+ // Find current track index in playlist
678
+ const currentTrackPath = playlist[currentTrack].path;
679
+ const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath);
680
+
681
+ if (playlistTrackIndex > 0) {
682
+ // Play previous track in playlist
683
+ const prevTrack = currentPlaylist.tracks[playlistTrackIndex - 1];
684
+ const mainPlaylistIndex = playlist.findIndex(t => t.path === prevTrack.path);
685
+ if (mainPlaylistIndex !== -1) {
686
+ playTrack(mainPlaylistIndex);
687
+ return;
688
+ }
689
+ }
690
+ }
691
+
692
+ // If no playlist or at start of playlist, play previous in main playlist
693
+ if (currentTrack > 0) {
694
+ playTrack(currentTrack - 1);
695
+ }
696
+ }
697
+
698
+ // Add this function to initialize audio player event listeners
699
+ function initializeAudioPlayer() {
700
+ audioPlayer.addEventListener('ended', () => {
701
+ playNext();
702
+ });
703
+
704
+ audioPlayer.addEventListener('error', (e) => {
705
+ console.error('Audio player error:', e);
706
+ showNotification('Error playing track');
707
+ playNext(); // Skip to next track on error
708
+ });
709
+
710
+ // Add this timeupdate listener here instead of in DOMContentLoaded
711
+ audioPlayer.addEventListener('timeupdate', () => {
712
+ const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100;
713
+ seekSlider.value = percent;
714
+ currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
715
+ updateActiveLyricLine(audioPlayer.currentTime);
716
+ });
717
+ }
718
+
719
+ // Update the initialization code
720
+ document.addEventListener('DOMContentLoaded', function() {
721
+ initializeAudioPlayer();
722
+
723
+ document.querySelectorAll('.queue-item-actions button').forEach(button => {
724
+ button.addEventListener('click', (e) => {
725
+ e.stopPropagation();
726
+ });
727
+ });
728
+
729
+ // Add lyrics toggle handler
730
+ document.getElementById('lyrics-toggle').addEventListener('click', () => {
731
+ syncedLyricsMode = !syncedLyricsMode;
732
+ if (currentTrack !== null) {
733
+ updatePlayerInfo(playlist[currentTrack]);
734
+ }
735
+ });
736
+ });
737
+
738
+ function renderAllSongs() {
739
+ const container = document.getElementById('view-container');
740
+ const viewTitle = document.getElementById('view-title');
741
+ viewTitle.textContent = 'All Songs';
742
+
743
+ container.innerHTML = `
744
+ <div class="track-list">
745
+ ${playlist.map((track, index) => `
746
+ <div class="music-item">
747
+ <div class="track-info" onclick="playFromMainPlaylist(${index})">
748
+ <div class="track-name">${track.metadata?.title || track.name}</div>
749
+ <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div>
750
+ </div>
751
+ <div class="track-actions">
752
+ <button class="menu-trigger" onclick="showTrackMenu(event, ${index})">
753
+ <i class="fas fa-ellipsis-v"></i>
754
+ </button>
755
+ </div>
756
+ </div>
757
+ `).join('')}
758
+ </div>
759
+ `;
760
+ }
761
+
762
+ function filterContent(query) {
763
+ query = query.toLowerCase();
764
+ const filteredTracks = playlist.filter(track =>
765
+ (track.metadata?.title || track.name).toLowerCase().includes(query) ||
766
+ (track.metadata?.artist || 'Unknown').toLowerCase().includes(query) ||
767
+ (track.metadata?.album || 'Unknown').toLowerCase().includes(query)
768
+ );
769
+
770
+ const container = document.getElementById('view-container');
771
+ const viewTitle = document.getElementById('view-title');
772
+ viewTitle.textContent = 'Search Results';
773
+
774
+ container.innerHTML = `
775
+ <div class="track-list">
776
+ ${filteredTracks.map((track, index) => `
777
+ <div class="music-item">
778
+ <div class="track-info" onclick="playTrack(${playlist.indexOf(track)})">
779
+ <div class="track-name">${track.metadata?.title || track.name}</div>
780
+ <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div>
781
+ </div>
782
+ <div class="track-actions">
783
+ <button class="menu-trigger" onclick="showTrackMenu(event, ${playlist.indexOf(track)})">
784
+ <i class="fas fa-ellipsis-v"></i>
785
+ </button>
786
+ </div>
787
+ </div>
788
+ `).join('')}
789
+ </div>
790
+ `;
791
+ }
792
+
793
+ function renderPlaylistView() {
794
+ const container = document.getElementById('view-container');
795
+ const viewTitle = document.getElementById('view-title');
796
+ viewTitle.textContent = 'Playlists';
797
+
798
+ container.innerHTML = `
799
+ <div class="playlist-grid">
800
+ ${Object.values(playlists).map(playlist => `
801
+ <div class="playlist-card" onclick="showPlaylist('${playlist.name}')">
802
+ <div class="playlist-art">
803
+ <i class="fas fa-music"></i>
804
+ </div>
805
+ <h3>${playlist.name}</h3>
806
+ <p>${playlist.tracks.length} tracks</p>
807
+ <button class="delete-playlist" onclick="deletePlaylist(event, '${playlist.name}')">
808
+ <i class="fas fa-trash"></i>
809
+ </button>
810
+ </div>
811
+ `).join('')}
812
+ </div>
813
+ `;
814
+ }
815
+
816
+ function deletePlaylist(event, playlistName) {
817
+ event.stopPropagation();
818
+ if (confirm(`Delete playlist "${playlistName}"?`)) {
819
+ delete playlists[playlistName];
820
+ localStorage.setItem('playlists', JSON.stringify(playlists));
821
+ renderPlaylistView();
822
+ renderPlaylists();
823
+ showNotification('Playlist deleted');
824
+ }
825
+ }
826
+
827
+ function removeFromPlaylist(playlistName, index) {
828
+ if (!playlists[playlistName]) return;
829
+
830
+ playlists[playlistName].tracks.splice(index, 1);
831
+ localStorage.setItem('playlists', JSON.stringify(playlists));
832
+
833
+ // Refresh the current playlist view
834
+ showPlaylist(playlistName);
835
+ renderPlaylists();
836
+ showNotification('Track removed from playlist');
837
+ }
838
+
839
+ function showNotification(message) {
840
+ const notification = document.createElement('div');
841
+ notification.className = 'notification';
842
+ notification.textContent = message;
843
+ document.body.appendChild(notification);
844
+
845
+ setTimeout(() => {
846
+ notification.classList.add('fade-out');
847
+ setTimeout(() => notification.remove(), 300);
848
+ }, 2000);
849
+ }
850
+
851
+ // Add these new functions to handle playing albums and artists
852
+ function playAlbum(albumName) {
853
+ const tracks = playlist.filter(track =>
854
+ track.metadata?.album === albumName
855
+ );
856
+ if (tracks.length > 0) {
857
+ // Create a temporary playlist for the album
858
+ currentPlaylist = {
859
+ name: albumName,
860
+ tracks: tracks
861
+ };
862
+ isPlayingPlaylist = true;
863
+
864
+ // Play the first track
865
+ const mainPlaylistIndex = playlist.indexOf(tracks[0]);
866
+ if (mainPlaylistIndex !== -1) {
867
+ playTrack(mainPlaylistIndex);
868
+ showNotification(`Playing album: ${albumName}`);
869
+ }
870
+ }
871
+ }
872
+
873
+ function playArtist(artistName) {
874
+ const tracks = playlist.filter(track =>
875
+ track.metadata?.artist === artistName
876
+ );
877
+ if (tracks.length > 0) {
878
+ // Create a temporary playlist for the artist
879
+ currentPlaylist = {
880
+ name: artistName,
881
+ tracks: tracks
882
+ };
883
+ isPlayingPlaylist = true;
884
+
885
+ // Play the first track
886
+ const mainPlaylistIndex = playlist.indexOf(tracks[0]);
887
+ if (mainPlaylistIndex !== -1) {
888
+ playTrack(mainPlaylistIndex);
889
+ showNotification(`Playing artist: ${artistName}`);
890
+ }
891
+ }
892
+ }
893
+
894
+ // Add this new function to handle playing from main playlist
895
+ function playFromMainPlaylist(index) {
896
+ isPlayingPlaylist = false;
897
+ currentPlaylist = null;
898
+ playTrack(index);
899
+ }