AdityaAdaki commited on
Commit
25201bd
·
0 Parent(s):

Initial commit: Music Player Web App

Browse files
Files changed (8) hide show
  1. .gitattributes +2 -0
  2. Dockerfile +22 -0
  3. README.md +24 -0
  4. app.py +309 -0
  5. requirements.txt +2 -0
  6. static/player.js +899 -0
  7. static/style.css +692 -0
  8. templates/index.html +108 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.flac filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+ COPY ./app.py /code/app.py
7
+
8
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
9
+
10
+ # Create music directory
11
+ RUN mkdir /code/music
12
+
13
+ # Create static directory and add default cover
14
+ RUN mkdir /code/static
15
+ COPY ./static/default-cover.png /code/static/default-cover.png
16
+
17
+ # Copy templates
18
+ COPY ./templates /code/templates
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Music Player Web App
3
+ emoji: 🎵
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ 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
app.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory, jsonify, request
2
+ import os
3
+ 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
+ print(f"Attempting to read metadata from: {file_path}") # Debug log
18
+ try:
19
+ if file_path.endswith('.flac'):
20
+ audio = FLAC(file_path)
21
+ # FLAC metadata handling
22
+ metadata = {
23
+ 'title': str(audio.get('title', [os.path.basename(file_path)])[0]),
24
+ 'artist': str(audio.get('artist', ['Unknown'])[0]),
25
+ 'album': str(audio.get('album', ['Unknown'])[0]),
26
+ 'duration': int(audio.info.length),
27
+ 'sample_rate': audio.info.sample_rate,
28
+ 'channels': audio.info.channels,
29
+ 'artwork': None,
30
+ 'lyrics': str(audio.get('lyrics', [''])[0]),
31
+ 'synchronized_lyrics': get_synchronized_lyrics(audio)
32
+ }
33
+
34
+ # Handle FLAC artwork
35
+ if audio.pictures:
36
+ try:
37
+ import base64
38
+ artwork_data = audio.pictures[0].data
39
+ artwork_base64 = base64.b64encode(artwork_data).decode('utf-8')
40
+ metadata['artwork'] = f"data:image/jpeg;base64,{artwork_base64}"
41
+ print(f"Found FLAC artwork in {file_path}")
42
+ except Exception as e:
43
+ print(f"Error processing FLAC artwork: {str(e)}")
44
+
45
+ print(f"FLAC metadata extracted: {metadata}")
46
+ return metadata
47
+
48
+ elif file_path.endswith('.mp3'):
49
+ audio = MP3(file_path)
50
+ metadata = {
51
+ 'title': audio.tags.get('TIT2', [os.path.basename(file_path)])[0] if hasattr(audio, 'tags') else os.path.basename(file_path),
52
+ 'artist': audio.tags.get('TPE1', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
53
+ 'album': audio.tags.get('TALB', ['Unknown'])[0] if hasattr(audio, 'tags') else 'Unknown',
54
+ 'duration': int(audio.info.length),
55
+ 'sample_rate': audio.info.sample_rate,
56
+ 'channels': audio.info.channels,
57
+ 'artwork': None,
58
+ 'lyrics': get_mp3_lyrics(audio),
59
+ 'synchronized_lyrics': get_mp3_synchronized_lyrics(audio)
60
+ }
61
+
62
+ # Handle MP3 artwork
63
+ if hasattr(audio, 'tags'):
64
+ try:
65
+ import base64
66
+ apic_keys = [k for k in audio.tags.keys() if k.startswith('APIC:')]
67
+ if apic_keys:
68
+ artwork_data = audio.tags[apic_keys[0]].data
69
+ artwork_base64 = base64.b64encode(artwork_data).decode('utf-8')
70
+ metadata['artwork'] = f"data:image/jpeg;base64,{artwork_base64}"
71
+ print(f"Found MP3 artwork in {file_path}")
72
+ except Exception as e:
73
+ print(f"Error processing MP3 artwork: {str(e)}")
74
+
75
+ print(f"MP3 metadata extracted: {metadata}")
76
+ return metadata
77
+
78
+ elif file_path.endswith('.wav'):
79
+ audio = WAVE(file_path)
80
+ metadata = {
81
+ 'title': os.path.basename(file_path),
82
+ 'artist': 'Unknown',
83
+ 'album': 'Unknown',
84
+ 'duration': int(audio.info.length),
85
+ 'sample_rate': audio.info.sample_rate,
86
+ 'channels': audio.info.channels,
87
+ 'artwork': None,
88
+ 'lyrics': '',
89
+ 'synchronized_lyrics': []
90
+ }
91
+ print(f"WAV metadata extracted: {metadata}")
92
+ return metadata
93
+ else:
94
+ return None
95
+
96
+ except Exception as e:
97
+ print(f"Error reading metadata for {file_path}: {str(e)}")
98
+ return {
99
+ 'title': os.path.basename(file_path),
100
+ 'artist': 'Unknown',
101
+ 'album': 'Unknown',
102
+ 'duration': 0,
103
+ 'sample_rate': 0,
104
+ 'channels': 0,
105
+ 'artwork': None
106
+ }
107
+
108
+ def get_folder_structure(path):
109
+ print(f"Scanning directory: {path}") # Debug log
110
+ try:
111
+ if not os.path.exists(path):
112
+ print(f"Directory does not exist: {path}")
113
+ return []
114
+
115
+ structure = []
116
+ files = os.listdir(path)
117
+ print(f"Found {len(files)} items in {path}")
118
+
119
+ for item in files:
120
+ item_path = os.path.join(path, item)
121
+ if os.path.isfile(item_path) and item.endswith(('.mp3', '.wav', '.flac')):
122
+ print(f"\nProcessing audio file: {item_path}")
123
+ metadata = get_audio_metadata(item_path)
124
+ if metadata:
125
+ print(f"Title: {metadata.get('title')}")
126
+ print(f"Artist: {metadata.get('artist')}")
127
+ print(f"Album: {metadata.get('album')}")
128
+ print(f"Duration: {metadata.get('duration')} seconds")
129
+ print(f"Sample rate: {metadata.get('sample_rate')} Hz")
130
+ print(f"Channels: {metadata.get('channels')}")
131
+ print(f"Has artwork: {'Yes' if metadata.get('artwork') else 'No'}")
132
+
133
+ structure.append({
134
+ 'type': 'file',
135
+ 'name': item,
136
+ 'path': os.path.relpath(item_path, 'music'),
137
+ 'metadata': metadata
138
+ })
139
+ elif os.path.isdir(item_path):
140
+ structure.append({
141
+ 'type': 'folder',
142
+ 'name': item,
143
+ 'path': os.path.relpath(item_path, 'music'),
144
+ 'contents': get_folder_structure(item_path)
145
+ })
146
+ return structure
147
+ except Exception as e:
148
+ print(f"Error scanning directory {path}: {str(e)}")
149
+ return []
150
+
151
+ @app.route('/music/<path:filename>')
152
+ def serve_music(filename):
153
+ print(f"Attempting to serve: {filename}") # Debug log
154
+ return send_from_directory('music', filename)
155
+
156
+ @app.route('/api/music-structure')
157
+ def get_music_structure():
158
+ music_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'music')
159
+ print(f"Music directory path: {music_dir}") # Debug log
160
+
161
+ if not os.path.exists(music_dir):
162
+ print(f"Creating music directory: {music_dir}") # Debug log
163
+ os.makedirs(music_dir)
164
+ return jsonify([]) # Return empty list if no music directory
165
+
166
+ if not os.listdir(music_dir):
167
+ print(f"Music directory is empty: {music_dir}") # Debug log
168
+ return jsonify([]) # Return empty list if directory is empty
169
+
170
+ structure = get_folder_structure(music_dir)
171
+ print(f"Found structure: {structure}") # Debug log
172
+ return jsonify(structure)
173
+
174
+ @app.route('/')
175
+ def index():
176
+ return render_template('index.html')
177
+
178
+ @app.route('/static/default-cover.png')
179
+ def default_cover():
180
+ return send_from_directory('static', 'default-cover.png')
181
+
182
+ @app.route('/api/playlists', methods=['GET', 'POST', 'DELETE'])
183
+ def manage_playlists():
184
+ global playlists
185
+ if request.method == 'GET':
186
+ return jsonify(playlists)
187
+
188
+ if request.method == 'POST':
189
+ data = request.get_json()
190
+ playlist_name = data.get('name')
191
+ songs = data.get('songs', [])
192
+
193
+ if playlist_name:
194
+ playlists[playlist_name] = songs
195
+ return jsonify({'message': f'Playlist {playlist_name} created/updated'})
196
+
197
+ return jsonify({'error': 'No playlist name provided'}), 400
198
+
199
+ if request.method == 'DELETE':
200
+ playlist_name = request.args.get('name')
201
+ if playlist_name in playlists:
202
+ del playlists[playlist_name]
203
+ return jsonify({'message': f'Playlist {playlist_name} deleted'})
204
+
205
+ return jsonify({'error': 'Playlist not found'}), 404
206
+
207
+ @app.route('/api/playlists/<name>/add', methods=['POST'])
208
+ def add_to_playlist(name):
209
+ data = request.get_json()
210
+ songs = data.get('songs', [])
211
+
212
+ if name not in playlists:
213
+ playlists[name] = []
214
+
215
+ playlists[name].extend(songs)
216
+ return jsonify(playlists[name])
217
+
218
+ @app.route('/api/playlists/<name>/remove', methods=['POST'])
219
+ def remove_from_playlist(name):
220
+ if name not in playlists:
221
+ return jsonify({'error': 'Playlist not found'}), 404
222
+
223
+ data = request.get_json()
224
+ indices = data.get('indices', [])
225
+
226
+ # Remove songs in reverse order to avoid index shifting
227
+ for index in sorted(indices, reverse=True):
228
+ if 0 <= index < len(playlists[name]):
229
+ playlists[name].pop(index)
230
+
231
+ return jsonify(playlists[name])
232
+
233
+ def get_synchronized_lyrics(audio):
234
+ """Extract synchronized lyrics from FLAC metadata"""
235
+ try:
236
+ if 'LYRICS' in audio.tags:
237
+ # Try to parse synchronized lyrics in LRC format
238
+ lyrics = audio.tags['LYRICS'][0]
239
+ return parse_lrc_format(lyrics)
240
+ except Exception as e:
241
+ print(f"Error parsing synchronized lyrics: {str(e)}")
242
+ return []
243
+
244
+ def get_mp3_lyrics(audio):
245
+ """Extract plain lyrics from MP3 metadata"""
246
+ try:
247
+ if hasattr(audio, 'tags'):
248
+ # Try different common lyrics tag formats
249
+ for tag in ['USLT::', 'LYRICS', 'LYRICS:']:
250
+ if tag in audio.tags:
251
+ return str(audio.tags[tag])
252
+ except Exception as e:
253
+ print(f"Error extracting MP3 lyrics: {str(e)}")
254
+ return ''
255
+
256
+ def get_mp3_synchronized_lyrics(audio):
257
+ """Extract synchronized lyrics from MP3 metadata"""
258
+ try:
259
+ if hasattr(audio, 'tags'):
260
+ # Look for SYLT (Synchronized Lyrics) frames
261
+ sylt_frames = [f for f in audio.tags if f.startswith('SYLT:')]
262
+ if sylt_frames:
263
+ sylt = audio.tags[sylt_frames[0]]
264
+ return parse_sylt_format(sylt)
265
+ except Exception as e:
266
+ print(f"Error extracting MP3 synchronized lyrics: {str(e)}")
267
+ return []
268
+
269
+ def parse_lrc_format(lrc_text):
270
+ """Parse LRC format synchronized lyrics"""
271
+ lyrics = []
272
+ for line in lrc_text.split('\n'):
273
+ if line.strip():
274
+ try:
275
+ # LRC format: [mm:ss.xx]lyrics text
276
+ time_str = line[1:line.find(']')]
277
+ text = line[line.find(']')+1:].strip()
278
+
279
+ # Convert time to seconds
280
+ if '.' in time_str:
281
+ mm_ss, ms = time_str.split('.')
282
+ else:
283
+ mm_ss, ms = time_str, '0'
284
+ minutes, seconds = map(int, mm_ss.split(':'))
285
+ timestamp = minutes * 60 + seconds + float(f'0.{ms}')
286
+
287
+ lyrics.append({'time': timestamp, 'text': text})
288
+ except:
289
+ continue
290
+ return sorted(lyrics, key=lambda x: x['time'])
291
+
292
+ def parse_sylt_format(sylt):
293
+ """Parse SYLT format synchronized lyrics"""
294
+ lyrics = []
295
+ for time, text in sylt.text:
296
+ lyrics.append({
297
+ 'time': time / 1000.0, # Convert milliseconds to seconds
298
+ 'text': text
299
+ })
300
+ return sorted(lyrics, key=lambda x: x['time'])
301
+
302
+ if __name__ == '__main__':
303
+ # Create music directory if it doesn't exist
304
+ if not os.path.exists(app.config['MUSIC_FOLDER']):
305
+ os.makedirs(app.config['MUSIC_FOLDER'])
306
+
307
+ # Use environment variables for host and port
308
+ port = int(os.environ.get('PORT', 7860))
309
+ app.run(host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ mutagen
static/player.js ADDED
@@ -0,0 +1,899 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let currentTrack = null;
2
+ let isPlaying = false;
3
+ let playlist = [];
4
+ let currentView = 'all';
5
+ let playlists = JSON.parse(localStorage.getItem('playlists')) || {};
6
+ let currentPlaylist = null;
7
+ let currentLyrics = null;
8
+ let isPlayingPlaylist = false;
9
+
10
+ const audioPlayer = document.getElementById('audio-player');
11
+ const playBtn = document.getElementById('play-btn');
12
+ const prevBtn = document.getElementById('prev-btn');
13
+ const nextBtn = document.getElementById('next-btn');
14
+ const seekSlider = document.getElementById('seek-slider');
15
+ const volumeSlider = document.getElementById('volume-slider');
16
+ const currentTimeSpan = document.getElementById('current-time');
17
+ const durationSpan = document.getElementById('duration');
18
+
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 = '') {
31
+ let flat = [];
32
+ structure.forEach(item => {
33
+ if (item.type === 'file') {
34
+ item.fullPath = prefix + item.path;
35
+ flat.push(item);
36
+ } else if (item.type === 'folder') {
37
+ flat = flat.concat(flattenPlaylist(item.contents, item.path + '/'));
38
+ }
39
+ });
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
+ }
static/style.css ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: #f0f2f5;
10
+ color: #333;
11
+ }
12
+
13
+ .container {
14
+ display: grid;
15
+ grid-template-columns: 250px 1fr;
16
+ grid-template-rows: 1fr auto;
17
+ gap: 1rem;
18
+ height: 100vh;
19
+ padding: 1rem;
20
+ }
21
+
22
+ .music-player {
23
+ grid-column: 1 / -1;
24
+ background: white;
25
+ border-radius: 12px;
26
+ padding: 2rem;
27
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
28
+ }
29
+
30
+ .player-header h1 {
31
+ margin-bottom: 2rem;
32
+ color: #1a73e8;
33
+ }
34
+
35
+ .now-playing {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 2rem;
39
+ margin-bottom: 2rem;
40
+ }
41
+
42
+ #album-art {
43
+ width: 200px;
44
+ height: 200px;
45
+ border-radius: 8px;
46
+ object-fit: cover;
47
+ }
48
+
49
+ .track-info {
50
+ flex: 1;
51
+ cursor: pointer;
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 0.25rem;
55
+ }
56
+
57
+ #track-title {
58
+ font-size: 1.5rem;
59
+ font-weight: bold;
60
+ margin-bottom: 0.5rem;
61
+ }
62
+
63
+ #track-artist, #track-album {
64
+ color: #666;
65
+ margin-bottom: 0.5rem;
66
+ }
67
+
68
+ .player-controls {
69
+ width: 100%;
70
+ }
71
+
72
+ .time-control {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 1rem;
76
+ margin-bottom: 1rem;
77
+ }
78
+
79
+ #seek-slider {
80
+ flex: 1;
81
+ }
82
+
83
+ .control-buttons {
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ gap: 1rem;
88
+ }
89
+
90
+ button {
91
+ background: none;
92
+ border: none;
93
+ cursor: pointer;
94
+ font-size: 1.5rem;
95
+ color: #1a73e8;
96
+ padding: 0.5rem;
97
+ border-radius: 50%;
98
+ transition: background-color 0.3s;
99
+ }
100
+
101
+ button:hover {
102
+ background-color: #f0f2f5;
103
+ }
104
+
105
+ .volume-control {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 0.5rem;
109
+ margin-left: 2rem;
110
+ }
111
+
112
+ #volume-slider {
113
+ width: 100px;
114
+ }
115
+
116
+ .sidebar {
117
+ background: white;
118
+ border-radius: 12px;
119
+ padding: 1rem;
120
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
121
+ }
122
+
123
+ .nav-menu, .playlist-menu {
124
+ list-style: none;
125
+ padding: 0;
126
+ margin: 1rem 0;
127
+ }
128
+
129
+ .nav-menu li a, .playlist-menu li a {
130
+ display: block;
131
+ padding: 0.75rem 1rem;
132
+ color: #333;
133
+ text-decoration: none;
134
+ border-radius: 6px;
135
+ transition: background-color 0.3s;
136
+ }
137
+
138
+ .nav-menu li a:hover, .playlist-menu li a:hover {
139
+ background-color: #f0f2f5;
140
+ }
141
+
142
+ .nav-menu li a.active {
143
+ background-color: #e8f0fe;
144
+ color: #1a73e8;
145
+ }
146
+
147
+ .btn-new-playlist {
148
+ width: 100%;
149
+ padding: 0.75rem;
150
+ background: none;
151
+ border: 1px dashed #1a73e8;
152
+ color: #1a73e8;
153
+ border-radius: 6px;
154
+ cursor: pointer;
155
+ transition: all 0.3s;
156
+ }
157
+
158
+ .btn-new-playlist:hover {
159
+ background: #e8f0fe;
160
+ }
161
+
162
+ .main-content {
163
+ background: white;
164
+ border-radius: 12px;
165
+ padding: 1rem;
166
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
167
+ overflow-y: auto;
168
+ }
169
+
170
+ .view-header {
171
+ display: flex;
172
+ justify-content: space-between;
173
+ align-items: center;
174
+ margin-bottom: 2rem;
175
+ }
176
+
177
+ .search-box {
178
+ position: relative;
179
+ }
180
+
181
+ .search-box input {
182
+ padding: 0.5rem 2rem 0.5rem 1rem;
183
+ border: 1px solid #ddd;
184
+ border-radius: 20px;
185
+ width: 200px;
186
+ }
187
+
188
+ .search-box i {
189
+ position: absolute;
190
+ right: 0.75rem;
191
+ top: 50%;
192
+ transform: translateY(-50%);
193
+ color: #666;
194
+ }
195
+
196
+ .album-grid, .artist-grid {
197
+ display: grid;
198
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
199
+ gap: 1.5rem;
200
+ padding: 1rem;
201
+ }
202
+
203
+ .album-card, .artist-card {
204
+ text-align: center;
205
+ cursor: pointer;
206
+ transition: transform 0.3s;
207
+ }
208
+
209
+ .album-card:hover, .artist-card:hover {
210
+ transform: translateY(-5px);
211
+ }
212
+
213
+ .album-art, .artist-image {
214
+ width: 180px;
215
+ height: 180px;
216
+ border-radius: 8px;
217
+ margin-bottom: 0.5rem;
218
+ object-fit: cover;
219
+ }
220
+
221
+ .track-list {
222
+ display: flex;
223
+ flex-direction: column;
224
+ gap: 0.5rem;
225
+ }
226
+
227
+ .music-item {
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ padding: 0.75rem;
232
+ border-radius: 6px;
233
+ transition: background-color 0.3s;
234
+ }
235
+
236
+ .music-item:hover {
237
+ background-color: #f0f2f5;
238
+ }
239
+
240
+ .music-item.playing {
241
+ background-color: #e8f0fe;
242
+ color: #1a73e8;
243
+ }
244
+
245
+ input[type="range"] {
246
+ -webkit-appearance: none;
247
+ appearance: none;
248
+ height: 5px;
249
+ background: #ddd;
250
+ border-radius: 5px;
251
+ cursor: pointer;
252
+ }
253
+
254
+ input[type="range"]::-webkit-slider-thumb {
255
+ -webkit-appearance: none;
256
+ width: 15px;
257
+ height: 15px;
258
+ background: #1a73e8;
259
+ border-radius: 50%;
260
+ }
261
+
262
+ .modal {
263
+ display: none;
264
+ position: fixed;
265
+ top: 0;
266
+ left: 0;
267
+ width: 100%;
268
+ height: 100%;
269
+ background: rgba(0, 0, 0, 0.5);
270
+ align-items: center;
271
+ justify-content: center;
272
+ }
273
+
274
+ .modal-content {
275
+ background: white;
276
+ padding: 2rem;
277
+ border-radius: 12px;
278
+ width: 400px;
279
+ }
280
+
281
+ .modal-content input {
282
+ width: 100%;
283
+ padding: 0.75rem;
284
+ margin: 1rem 0;
285
+ border: 1px solid #ddd;
286
+ border-radius: 6px;
287
+ }
288
+
289
+ .modal-buttons {
290
+ display: flex;
291
+ justify-content: flex-end;
292
+ gap: 1rem;
293
+ }
294
+
295
+ .modal-buttons button {
296
+ padding: 0.5rem 1rem;
297
+ border-radius: 6px;
298
+ cursor: pointer;
299
+ }
300
+
301
+ #save-playlist {
302
+ background: #1a73e8;
303
+ color: white;
304
+ border: none;
305
+ }
306
+
307
+ #cancel-playlist {
308
+ background: none;
309
+ border: 1px solid #ddd;
310
+ }
311
+
312
+ .track-actions {
313
+ display: flex;
314
+ align-items: center;
315
+ }
316
+
317
+ .track-name {
318
+ font-weight: 500;
319
+ }
320
+
321
+ .track-artist {
322
+ color: #666;
323
+ font-size: 0.9em;
324
+ }
325
+
326
+ .playlist-menu-popup {
327
+ background: white;
328
+ border-radius: 8px;
329
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
330
+ z-index: 1000;
331
+ min-width: 200px;
332
+ }
333
+
334
+ .menu-items {
335
+ padding: 0.5rem;
336
+ }
337
+
338
+ .menu-item {
339
+ padding: 0.75rem 1rem;
340
+ cursor: pointer;
341
+ border-radius: 4px;
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 0.5rem;
345
+ }
346
+
347
+ .menu-item:hover {
348
+ background-color: #f0f2f5;
349
+ }
350
+
351
+ .notification {
352
+ position: fixed;
353
+ bottom: 20px;
354
+ right: 20px;
355
+ background: #1a73e8;
356
+ color: white;
357
+ padding: 0.75rem 1.5rem;
358
+ border-radius: 4px;
359
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
360
+ animation: slide-in 0.3s ease-out;
361
+ z-index: 1000;
362
+ }
363
+
364
+ .notification.fade-out {
365
+ animation: fade-out 0.3s ease-out;
366
+ }
367
+
368
+ .playlist-grid {
369
+ display: grid;
370
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
371
+ gap: 1.5rem;
372
+ padding: 1rem;
373
+ }
374
+
375
+ .playlist-card {
376
+ background: white;
377
+ border-radius: 8px;
378
+ padding: 1rem;
379
+ text-align: center;
380
+ cursor: pointer;
381
+ position: relative;
382
+ transition: transform 0.3s;
383
+ }
384
+
385
+ .playlist-card:hover {
386
+ transform: translateY(-5px);
387
+ }
388
+
389
+ .playlist-art {
390
+ width: 150px;
391
+ height: 150px;
392
+ background: #f0f2f5;
393
+ border-radius: 8px;
394
+ margin: 0 auto 1rem;
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ }
399
+
400
+ .playlist-art i {
401
+ font-size: 3rem;
402
+ color: #1a73e8;
403
+ }
404
+
405
+ .delete-playlist {
406
+ position: absolute;
407
+ top: 0.5rem;
408
+ right: 0.5rem;
409
+ background: none;
410
+ border: none;
411
+ color: #666;
412
+ padding: 0.5rem;
413
+ cursor: pointer;
414
+ opacity: 0;
415
+ transition: opacity 0.3s;
416
+ }
417
+
418
+ .playlist-card:hover .delete-playlist {
419
+ opacity: 1;
420
+ }
421
+
422
+ @keyframes slide-in {
423
+ from {
424
+ transform: translateX(100%);
425
+ opacity: 0;
426
+ }
427
+ to {
428
+ transform: translateX(0);
429
+ opacity: 1;
430
+ }
431
+ }
432
+
433
+ @keyframes fade-out {
434
+ from {
435
+ transform: translateX(0);
436
+ opacity: 1;
437
+ }
438
+ to {
439
+ transform: translateX(100%);
440
+ opacity: 0;
441
+ }
442
+ }
443
+
444
+ .menu-trigger {
445
+ padding: 0.5rem;
446
+ opacity: 0;
447
+ transition: opacity 0.3s;
448
+ color: #666;
449
+ }
450
+
451
+ .music-item:hover .menu-trigger {
452
+ opacity: 1;
453
+ }
454
+
455
+ .context-menu {
456
+ background: white;
457
+ border-radius: 8px;
458
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
459
+ z-index: 1000;
460
+ min-width: 200px;
461
+ overflow: hidden;
462
+ }
463
+
464
+ .menu-items {
465
+ padding: 0.5rem 0;
466
+ }
467
+
468
+ .menu-item {
469
+ padding: 0.75rem 1rem;
470
+ cursor: pointer;
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 0.75rem;
474
+ transition: background-color 0.2s;
475
+ }
476
+
477
+ .menu-item:hover {
478
+ background-color: #f0f2f5;
479
+ }
480
+
481
+ .menu-item i {
482
+ width: 20px;
483
+ text-align: center;
484
+ color: #666;
485
+ }
486
+
487
+ .menu-separator {
488
+ height: 1px;
489
+ background-color: #eee;
490
+ margin: 0.5rem 0;
491
+ }
492
+
493
+ .lyrics-container {
494
+ flex: 1;
495
+ max-width: 400px;
496
+ margin-left: 2rem;
497
+ display: flex;
498
+ flex-direction: column;
499
+ max-height: 200px;
500
+ }
501
+
502
+ .lyrics-header {
503
+ display: flex;
504
+ justify-content: space-between;
505
+ align-items: center;
506
+ margin-bottom: 1rem;
507
+ }
508
+
509
+ .lyrics-toggle {
510
+ background: none;
511
+ border: 1px solid #ddd;
512
+ padding: 0.5rem 1rem;
513
+ border-radius: 4px;
514
+ font-size: 0.9rem;
515
+ }
516
+
517
+ .lyrics-content {
518
+ flex: 1;
519
+ overflow-y: auto;
520
+ padding-right: 1rem;
521
+ line-height: 1.6;
522
+ scroll-behavior: smooth;
523
+ padding: 1rem;
524
+ }
525
+
526
+ .lyrics-line {
527
+ margin-bottom: 0.5rem;
528
+ transition: all 0.3s ease;
529
+ cursor: pointer;
530
+ padding: 0.5rem 1rem;
531
+ border-radius: 4px;
532
+ opacity: 0.7;
533
+ }
534
+
535
+ .lyrics-line:hover {
536
+ background-color: #f0f2f5;
537
+ opacity: 1;
538
+ }
539
+
540
+ .lyrics-line.active {
541
+ color: #1a73e8;
542
+ font-weight: bold;
543
+ opacity: 1;
544
+ background-color: #e8f0fe;
545
+ }
546
+
547
+ .playlist-header {
548
+ display: flex;
549
+ justify-content: space-between;
550
+ align-items: center;
551
+ padding: 1rem;
552
+ background: #f8f9fa;
553
+ border-radius: 8px;
554
+ margin-bottom: 1rem;
555
+ }
556
+
557
+ .playlist-info {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 1rem;
561
+ }
562
+
563
+ .playlist-icon {
564
+ font-size: 2.5rem;
565
+ color: #1a73e8;
566
+ background: #e8f0fe;
567
+ padding: 1rem;
568
+ border-radius: 8px;
569
+ }
570
+
571
+ .play-playlist {
572
+ background: #1a73e8;
573
+ color: white;
574
+ padding: 0.75rem 1.5rem;
575
+ border-radius: 20px;
576
+ font-size: 1rem;
577
+ }
578
+
579
+ .play-playlist:hover {
580
+ background: #1557b0;
581
+ }
582
+
583
+ .playlist-count {
584
+ color: #666;
585
+ font-size: 0.9em;
586
+ margin-left: 0.5rem;
587
+ }
588
+
589
+ #playlist-list a {
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: space-between;
593
+ }
594
+
595
+ #playlist-list i {
596
+ margin-right: 0.5rem;
597
+ }
598
+
599
+ .album-art-container, .artist-art-container {
600
+ position: relative;
601
+ width: 180px;
602
+ height: 180px;
603
+ margin-bottom: 0.5rem;
604
+ }
605
+
606
+ .album-art, .artist-image {
607
+ width: 100%;
608
+ height: 100%;
609
+ border-radius: 8px;
610
+ object-fit: cover;
611
+ }
612
+
613
+ .play-overlay {
614
+ position: absolute;
615
+ top: 50%;
616
+ left: 50%;
617
+ transform: translate(-50%, -50%);
618
+ background: rgba(26, 115, 232, 0.9);
619
+ color: white;
620
+ width: 50px;
621
+ height: 50px;
622
+ border-radius: 50%;
623
+ display: flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ opacity: 0;
627
+ transition: opacity 0.3s;
628
+ cursor: pointer;
629
+ border: none;
630
+ font-size: 1.2rem;
631
+ }
632
+
633
+ .album-art-container:hover .play-overlay,
634
+ .artist-art-container:hover .play-overlay {
635
+ opacity: 1;
636
+ }
637
+
638
+ .album-info, .artist-info {
639
+ cursor: pointer;
640
+ }
641
+
642
+ .album-card, .artist-card {
643
+ display: flex;
644
+ flex-direction: column;
645
+ align-items: center;
646
+ text-align: center;
647
+ }
648
+
649
+ .artist-image {
650
+ background-color: #e8f0fe;
651
+ }
652
+
653
+ .artist-image[src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg=="] {
654
+ background: #e8f0fe;
655
+ padding: 2rem;
656
+ }
657
+
658
+ .existing-playlists {
659
+ max-height: 200px;
660
+ overflow-y: auto;
661
+ margin-bottom: 1rem;
662
+ border-bottom: 1px solid #eee;
663
+ }
664
+
665
+ .existing-playlist-item {
666
+ padding: 0.75rem;
667
+ cursor: pointer;
668
+ display: flex;
669
+ align-items: center;
670
+ gap: 0.5rem;
671
+ border-radius: 4px;
672
+ transition: background-color 0.2s;
673
+ }
674
+
675
+ .existing-playlist-item:hover {
676
+ background-color: #f0f2f5;
677
+ }
678
+
679
+ .existing-playlist-item i {
680
+ color: #1a73e8;
681
+ }
682
+
683
+ .new-playlist-section {
684
+ padding-top: 1rem;
685
+ }
686
+
687
+ .new-playlist-section h3 {
688
+ margin-bottom: 0.75rem;
689
+ font-size: 1rem;
690
+ color: #666;
691
+ }
692
+
templates/index.html ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Music Player</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <div class="sidebar">
13
+ <nav>
14
+ <h2>Library</h2>
15
+ <ul class="nav-menu">
16
+ <li><a href="#" data-view="all" class="active">All Songs</a></li>
17
+ <li><a href="#" data-view="albums">Albums</a></li>
18
+ <li><a href="#" data-view="artists">Artists</a></li>
19
+ <li><a href="#" data-view="playlists">Playlists</a></li>
20
+ </ul>
21
+
22
+ <h3>Playlists</h3>
23
+ <ul id="playlist-list" class="playlist-menu">
24
+ <!-- Playlists will be added here dynamically -->
25
+ </ul>
26
+ <button id="new-playlist-btn" class="btn-new-playlist">
27
+ <i class="fas fa-plus"></i> New Playlist
28
+ </button>
29
+ </nav>
30
+ </div>
31
+
32
+ <div class="main-content">
33
+ <div class="view-header">
34
+ <h1 id="view-title">All Songs</h1>
35
+ <div class="search-box">
36
+ <input type="text" id="search-input" placeholder="Search...">
37
+ <i class="fas fa-search"></i>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="view-container">
42
+ <!-- Different views will be rendered here -->
43
+ </div>
44
+ </div>
45
+
46
+ <div class="music-player">
47
+ <div class="player-header">
48
+ <h1>Music Player</h1>
49
+ </div>
50
+
51
+ <div class="now-playing">
52
+ <img id="album-art" src="{{ url_for('static', filename='default-cover.png') }}" alt="Album Art">
53
+ <div class="track-info">
54
+ <div id="track-title">No track selected</div>
55
+ <div id="track-artist">-</div>
56
+ <div id="track-album">-</div>
57
+ </div>
58
+ <div class="lyrics-container">
59
+ <div class="lyrics-header">
60
+ <h3>Lyrics</h3>
61
+ </div>
62
+ <div id="lyrics-content" class="lyrics-content">
63
+ <!-- Lyrics will be displayed here -->
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="player-controls">
69
+ <div class="time-control">
70
+ <span id="current-time">0:00</span>
71
+ <input type="range" id="seek-slider" min="0" max="100" value="0">
72
+ <span id="duration">0:00</span>
73
+ </div>
74
+ <div class="control-buttons">
75
+ <button id="prev-btn"><i class="fas fa-backward"></i></button>
76
+ <button id="play-btn"><i class="fas fa-play"></i></button>
77
+ <button id="next-btn"><i class="fas fa-forward"></i></button>
78
+ <div class="volume-control">
79
+ <i class="fas fa-volume-up"></i>
80
+ <input type="range" id="volume-slider" min="0" max="100" value="100">
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Modal for creating/editing playlists -->
88
+ <div id="playlist-modal" class="modal">
89
+ <div class="modal-content">
90
+ <h2>Add to Playlist</h2>
91
+ <div class="existing-playlists">
92
+ <!-- Existing playlists will be listed here -->
93
+ </div>
94
+ <div class="new-playlist-section">
95
+ <h3>Create New Playlist</h3>
96
+ <input type="text" id="playlist-name" placeholder="Playlist Name">
97
+ </div>
98
+ <div class="modal-buttons">
99
+ <button id="cancel-playlist">Cancel</button>
100
+ <button id="save-playlist">Create New</button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <audio id="audio-player"></audio>
106
+ <script src="{{ url_for('static', filename='player.js') }}"></script>
107
+ </body>
108
+ </html>