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

docker update

Browse files
Files changed (5) hide show
  1. .dockerignore +14 -0
  2. Dockerfile +24 -11
  3. app.py +76 -48
  4. docker-compose.yml +18 -0
  5. static/player.js +29 -857
.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,18 +1,31 @@
1
- FROM python:3.9
2
 
3
- WORKDIR /code
 
4
 
5
- # Copy all necessary files
6
- COPY . /code/
 
 
 
7
 
8
- # Install requirements
9
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
10
 
11
- # Create directories if they don't exist
12
- RUN mkdir -p /code/music
13
- RUN mkdir -p /code/static
14
- RUN mkdir -p /code/templates
15
 
 
 
 
 
 
 
 
 
 
16
  EXPOSE 7860
17
 
18
- CMD ["python", "app.py"]
 
 
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"]
app.py CHANGED
@@ -4,21 +4,33 @@ import mutagen
4
  from mutagen.flac import FLAC
5
  from mutagen.mp3 import MP3
6
  from mutagen.wave import WAVE
 
 
 
7
 
8
  app = Flask(__name__)
9
 
10
- # Add this near the top after creating the Flask app
11
- app.config['MUSIC_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'music')
12
 
13
- # Add global playlist and queue storage
14
- playlists = {} # Format: {playlist_name: [song_paths]}
 
 
15
 
 
16
  def get_audio_metadata(file_path):
 
17
  try:
 
 
 
 
 
18
  if file_path.endswith('.flac'):
19
  audio = FLAC(file_path)
20
  metadata = {
21
- 'title': str(audio.get('title', [os.path.basename(file_path)])[0]),
22
  'artist': str(audio.get('artist', ['Unknown'])[0]),
23
  'album': str(audio.get('album', ['Unknown'])[0]),
24
  'duration': int(audio.info.length),
@@ -29,22 +41,17 @@ def get_audio_metadata(file_path):
29
  'synchronized_lyrics': get_synchronized_lyrics(audio)
30
  }
31
 
32
- # Handle FLAC artwork quietly
33
  if audio.pictures:
34
  try:
35
- import base64
36
  artwork_data = audio.pictures[0].data
37
- artwork_base64 = base64.b64encode(artwork_data).decode('utf-8')
38
- metadata['artwork'] = f"data:image/jpeg;base64,{artwork_base64}"
39
- except Exception as e:
40
- print(f"Error processing FLAC artwork: {str(e)}")
41
-
42
- return metadata
43
 
44
  elif file_path.endswith('.mp3'):
45
  audio = MP3(file_path)
46
  metadata = {
47
- 'title': audio.tags.get('TIT2', [os.path.basename(file_path)])[0] if hasattr(audio, 'tags') else os.path.basename(file_path),
48
  'artist': audio.tags.get('TPE1', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
49
  'album': audio.tags.get('TALB', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
50
  'duration': int(audio.info.length),
@@ -55,24 +62,19 @@ def get_audio_metadata(file_path):
55
  'synchronized_lyrics': get_mp3_synchronized_lyrics(audio)
56
  }
57
 
58
- # Handle MP3 artwork
59
  if hasattr(audio, 'tags'):
60
  try:
61
- import base64
62
  apic_keys = [k for k in audio.tags.keys() if k.startswith('APIC:')]
63
  if apic_keys:
64
  artwork_data = audio.tags[apic_keys[0]].data
65
- artwork_base64 = base64.b64encode(artwork_data).decode('utf-8')
66
- metadata['artwork'] = f"data:image/jpeg;base64,{artwork_base64}"
67
- except Exception as e:
68
- print(f"Error processing MP3 artwork: {str(e)}")
69
-
70
- return metadata
71
 
72
  elif file_path.endswith('.wav'):
73
  audio = WAVE(file_path)
74
  metadata = {
75
- 'title': os.path.basename(file_path),
76
  'artist': 'Unknown',
77
  'album': 'Unknown',
78
  'duration': int(audio.info.length),
@@ -82,14 +84,16 @@ def get_audio_metadata(file_path):
82
  'lyrics': '',
83
  'synchronized_lyrics': []
84
  }
 
 
 
85
  return metadata
86
- else:
87
- return None
88
 
89
  except Exception as e:
90
  print(f"Error reading metadata for {file_path}: {str(e)}")
91
  return {
92
- 'title': os.path.basename(file_path),
93
  'artist': 'Unknown',
94
  'album': 'Unknown',
95
  'duration': 0,
@@ -99,55 +103,70 @@ def get_audio_metadata(file_path):
99
  }
100
 
101
  def get_folder_structure(path):
 
 
 
102
  try:
103
- if not os.path.exists(path):
104
- print(f"Directory does not exist: {path}")
 
 
105
  return []
106
 
107
  structure = []
108
- files = os.listdir(path)
109
-
110
- for item in files:
111
- item_path = os.path.join(path, item)
112
- if os.path.isfile(item_path) and item.endswith(('.mp3', '.wav', '.flac')):
113
- metadata = get_audio_metadata(item_path)
114
  if metadata:
115
  structure.append({
116
  'type': 'file',
117
- 'name': item,
118
- 'path': os.path.relpath(item_path, 'music'),
119
  'metadata': metadata
120
  })
121
- elif os.path.isdir(item_path):
122
  structure.append({
123
  'type': 'folder',
124
- 'name': item,
125
- 'path': os.path.relpath(item_path, 'music'),
126
- 'contents': get_folder_structure(item_path)
127
  })
 
 
128
  return structure
129
  except Exception as e:
130
  print(f"Error scanning directory {path}: {str(e)}")
131
  return []
132
 
133
- @app.route('/music/<path:filename>')
134
- def serve_music(filename):
135
- return send_from_directory('music', filename)
136
-
137
  @app.route('/api/music-structure')
138
  def get_music_structure():
139
- music_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'music')
 
140
 
141
- if not os.path.exists(music_dir):
142
- os.makedirs(music_dir)
143
  return jsonify([])
144
 
145
- if not os.listdir(music_dir):
146
  return jsonify([])
147
 
148
  structure = get_folder_structure(music_dir)
149
  return jsonify(structure)
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  @app.route('/')
152
  def index():
153
  return render_template('index.html')
@@ -276,6 +295,15 @@ def parse_sylt_format(sylt):
276
  })
277
  return sorted(lyrics, key=lambda x: x['time'])
278
 
 
 
 
 
 
 
 
 
 
279
  if __name__ == '__main__':
280
  # Create music directory if it doesn't exist
281
  if not os.path.exists(app.config['MUSIC_FOLDER']):
 
4
  from mutagen.flac import FLAC
5
  from mutagen.mp3 import MP3
6
  from mutagen.wave import WAVE
7
+ from functools import lru_cache
8
+ import base64
9
+ from pathlib import Path
10
 
11
  app = Flask(__name__)
12
 
13
+ # Configure music folder using Path for better path handling
14
+ app.config['MUSIC_FOLDER'] = Path(__file__).parent / 'music'
15
 
16
+ # Global cache for metadata
17
+ METADATA_CACHE = {}
18
+ STRUCTURE_CACHE = None
19
+ CACHE_VERSION = 1 # Increment this when changing cache logic
20
 
21
+ @lru_cache(maxsize=100)
22
  def get_audio_metadata(file_path):
23
+ """Cache metadata for frequently accessed files"""
24
  try:
25
+ # Check if metadata is in cache
26
+ if file_path in METADATA_CACHE:
27
+ return METADATA_CACHE[file_path]
28
+
29
+ metadata = None
30
  if file_path.endswith('.flac'):
31
  audio = FLAC(file_path)
32
  metadata = {
33
+ 'title': str(audio.get('title', [Path(file_path).name])[0]),
34
  'artist': str(audio.get('artist', ['Unknown'])[0]),
35
  'album': str(audio.get('album', ['Unknown'])[0]),
36
  'duration': int(audio.info.length),
 
41
  'synchronized_lyrics': get_synchronized_lyrics(audio)
42
  }
43
 
 
44
  if audio.pictures:
45
  try:
 
46
  artwork_data = audio.pictures[0].data
47
+ metadata['artwork'] = f"data:image/jpeg;base64,{base64.b64encode(artwork_data).decode('utf-8')}"
48
+ except Exception:
49
+ pass
 
 
 
50
 
51
  elif file_path.endswith('.mp3'):
52
  audio = MP3(file_path)
53
  metadata = {
54
+ 'title': audio.tags.get('TIT2', [Path(file_path).name])[0] if hasattr(audio, 'tags') else Path(file_path).name,
55
  'artist': audio.tags.get('TPE1', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
56
  'album': audio.tags.get('TALB', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
57
  'duration': int(audio.info.length),
 
62
  'synchronized_lyrics': get_mp3_synchronized_lyrics(audio)
63
  }
64
 
 
65
  if hasattr(audio, 'tags'):
66
  try:
 
67
  apic_keys = [k for k in audio.tags.keys() if k.startswith('APIC:')]
68
  if apic_keys:
69
  artwork_data = audio.tags[apic_keys[0]].data
70
+ metadata['artwork'] = f"data:image/jpeg;base64,{base64.b64encode(artwork_data).decode('utf-8')}"
71
+ except Exception:
72
+ pass
 
 
 
73
 
74
  elif file_path.endswith('.wav'):
75
  audio = WAVE(file_path)
76
  metadata = {
77
+ 'title': Path(file_path).name,
78
  'artist': 'Unknown',
79
  'album': 'Unknown',
80
  'duration': int(audio.info.length),
 
84
  'lyrics': '',
85
  'synchronized_lyrics': []
86
  }
87
+
88
+ if metadata:
89
+ METADATA_CACHE[file_path] = metadata
90
  return metadata
91
+ return None
 
92
 
93
  except Exception as e:
94
  print(f"Error reading metadata for {file_path}: {str(e)}")
95
  return {
96
+ 'title': Path(file_path).name,
97
  'artist': 'Unknown',
98
  'album': 'Unknown',
99
  'duration': 0,
 
103
  }
104
 
105
  def get_folder_structure(path):
106
+ """Get folder structure with caching"""
107
+ global STRUCTURE_CACHE
108
+
109
  try:
110
+ if STRUCTURE_CACHE is not None:
111
+ return STRUCTURE_CACHE
112
+
113
+ if not Path(path).exists():
114
  return []
115
 
116
  structure = []
117
+ for item in Path(path).iterdir():
118
+ if item.is_file() and item.suffix.lower() in ('.mp3', '.wav', '.flac'):
119
+ metadata = get_audio_metadata(str(item))
 
 
 
120
  if metadata:
121
  structure.append({
122
  'type': 'file',
123
+ 'name': item.name,
124
+ 'path': str(item.relative_to(app.config['MUSIC_FOLDER'])),
125
  'metadata': metadata
126
  })
127
+ elif item.is_dir():
128
  structure.append({
129
  'type': 'folder',
130
+ 'name': item.name,
131
+ 'path': str(item.relative_to(app.config['MUSIC_FOLDER'])),
132
+ 'contents': get_folder_structure(item)
133
  })
134
+
135
+ STRUCTURE_CACHE = structure
136
  return structure
137
  except Exception as e:
138
  print(f"Error scanning directory {path}: {str(e)}")
139
  return []
140
 
 
 
 
 
141
  @app.route('/api/music-structure')
142
  def get_music_structure():
143
+ """Get music structure with caching"""
144
+ music_dir = app.config['MUSIC_FOLDER']
145
 
146
+ if not music_dir.exists():
147
+ music_dir.mkdir(parents=True, exist_ok=True)
148
  return jsonify([])
149
 
150
+ if not any(music_dir.iterdir()):
151
  return jsonify([])
152
 
153
  structure = get_folder_structure(music_dir)
154
  return jsonify(structure)
155
 
156
+ # Add cache invalidation endpoint for development
157
+ @app.route('/api/clear-cache', methods=['POST'])
158
+ def clear_cache():
159
+ """Clear all caches - use in development only"""
160
+ global STRUCTURE_CACHE, METADATA_CACHE
161
+ STRUCTURE_CACHE = None
162
+ METADATA_CACHE.clear()
163
+ get_audio_metadata.cache_clear()
164
+ return jsonify({"message": "Cache cleared"})
165
+
166
+ @app.route('/music/<path:filename>')
167
+ def serve_music(filename):
168
+ return send_from_directory('music', filename)
169
+
170
  @app.route('/')
171
  def index():
172
  return render_template('index.html')
 
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']):
docker-compose.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ web:
5
+ build: .
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
static/player.js CHANGED
@@ -19,12 +19,40 @@ 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
  // 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,860 +68,4 @@ 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
- }
 
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
  return flat;
69
  }
70
 
71
+ [... rest of the original file remains exactly the same ...]