AdityaAdaki commited on
Commit
9ecce35
·
1 Parent(s): 31fbee6
Files changed (4) hide show
  1. app.py +89 -18
  2. static/css/style.css +304 -1
  3. static/js/main.js +222 -80
  4. templates/index.html +5 -3
app.py CHANGED
@@ -13,6 +13,8 @@ import mutagen.mp3
13
  import mutagen.flac
14
  import mutagen.oggvorbis
15
  from werkzeug.utils import secure_filename
 
 
16
 
17
  app = Flask(__name__, static_folder='static')
18
  # Create a temporary directory for uploads
@@ -27,6 +29,10 @@ FILE_LIFETIME = timedelta(hours=1) # Files will be deleted after 1 hour
27
  # Store file creation times
28
  file_timestamps = {}
29
 
 
 
 
 
30
  def allowed_file(filename):
31
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
32
 
@@ -46,30 +52,66 @@ def extract_metadata(filepath):
46
  'artist': 'Unknown Artist',
47
  'album': None,
48
  'genre': None,
49
- 'date': None
 
50
  }
51
 
52
  # Get the original filename without timestamp prefix and extension
53
  original_filename = os.path.splitext(os.path.basename(filepath))[0]
54
  if '_' in original_filename:
55
- # Remove timestamp prefix (YYYYMMDD_HHMMSS_)
56
  original_filename = '_'.join(original_filename.split('_')[2:])
57
 
58
  try:
59
  # Handle MP3 files
60
  if isinstance(audio, mutagen.mp3.MP3):
61
  try:
62
- id3 = EasyID3(filepath)
 
 
 
 
 
 
 
63
  metadata.update({
64
- 'title': id3.get('title', [''])[0],
65
- 'artist': id3.get('artist', ['Unknown Artist'])[0],
66
- 'album': id3.get('album', [''])[0],
67
- 'genre': id3.get('genre', [''])[0],
68
- 'date': id3.get('date', [''])[0]
69
  })
70
- except:
71
- metadata['title'] = original_filename
72
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  # Handle FLAC files
74
  elif isinstance(audio, mutagen.flac.FLAC):
75
  metadata.update({
@@ -79,6 +121,14 @@ def extract_metadata(filepath):
79
  'genre': audio.tags.get('GENRE', [''])[0] if audio.tags and 'GENRE' in audio.tags else None,
80
  'date': audio.tags.get('DATE', [''])[0] if audio.tags and 'DATE' in audio.tags else None
81
  })
 
 
 
 
 
 
 
 
82
 
83
  # Handle OGG files
84
  elif isinstance(audio, mutagen.oggvorbis.OggVorbis):
@@ -89,18 +139,30 @@ def extract_metadata(filepath):
89
  'genre': audio.get('genre', [''])[0] if 'genre' in audio else None,
90
  'date': audio.get('date', [''])[0] if 'date' in audio else None
91
  })
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  # If title is not found or empty, use original filename
94
  if not metadata['title']:
95
  metadata['title'] = original_filename
96
 
97
  except Exception as e:
98
- app.logger.error(f"Error reading tags: {str(e)}")
99
  metadata['title'] = original_filename
100
 
101
  return metadata
102
  except Exception as e:
103
- app.logger.error(f"Error extracting metadata: {str(e)}")
104
  return {
105
  'title': os.path.splitext(os.path.basename(filepath))[0].split('_', 2)[-1],
106
  'artist': 'Unknown Artist'
@@ -149,14 +211,21 @@ def request_entity_too_large(error):
149
 
150
  @app.route('/upload', methods=['POST'])
151
  def upload_file():
 
 
152
  if 'files[]' not in request.files:
 
153
  return jsonify({'success': False, 'error': 'No files uploaded'}), 400
154
 
155
  files = request.files.getlist('files[]')
 
 
156
  if not files or all(file.filename == '' for file in files):
 
157
  return jsonify({'success': False, 'error': 'No selected files'}), 400
158
 
159
  if len(files) > app.config['MAX_FILES']:
 
160
  return jsonify({
161
  'success': False,
162
  'error': f"Maximum {app.config['MAX_FILES']} files can be uploaded at once"
@@ -164,6 +233,7 @@ def upload_file():
164
 
165
  results = []
166
  for file in files:
 
167
  if file and allowed_file(file.filename):
168
  try:
169
  filename = secure_filename(file.filename)
@@ -172,10 +242,12 @@ def upload_file():
172
 
173
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
174
  file.save(filepath)
 
175
 
176
  file_timestamps[filename] = datetime.now()
177
 
178
  if not os.path.exists(filepath):
 
179
  results.append({
180
  'filename': file.filename,
181
  'success': False,
@@ -183,8 +255,8 @@ def upload_file():
183
  })
184
  continue
185
 
186
- # Extract metadata
187
  metadata = extract_metadata(filepath)
 
188
 
189
  results.append({
190
  'filename': file.filename,
@@ -193,22 +265,21 @@ def upload_file():
193
  'metadata': metadata
194
  })
195
  except Exception as e:
196
- app.logger.error(f"Upload error for {file.filename}: {str(e)}")
197
  results.append({
198
  'filename': file.filename,
199
  'success': False,
200
  'error': 'Server error during upload'
201
  })
202
  else:
 
203
  results.append({
204
  'filename': file.filename,
205
  'success': False,
206
  'error': 'Invalid file type'
207
  })
208
 
209
- # Debug log to check the response
210
- app.logger.debug(f"Upload response: {results}")
211
-
212
  return jsonify({
213
  'success': True,
214
  'files': results
 
13
  import mutagen.flac
14
  import mutagen.oggvorbis
15
  from werkzeug.utils import secure_filename
16
+ import logging
17
+ import base64
18
 
19
  app = Flask(__name__, static_folder='static')
20
  # Create a temporary directory for uploads
 
29
  # Store file creation times
30
  file_timestamps = {}
31
 
32
+ # Configure logging
33
+ logging.basicConfig(level=logging.INFO)
34
+ logger = logging.getLogger(__name__)
35
+
36
  def allowed_file(filename):
37
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
38
 
 
52
  'artist': 'Unknown Artist',
53
  'album': None,
54
  'genre': None,
55
+ 'date': None,
56
+ 'artwork': None
57
  }
58
 
59
  # Get the original filename without timestamp prefix and extension
60
  original_filename = os.path.splitext(os.path.basename(filepath))[0]
61
  if '_' in original_filename:
 
62
  original_filename = '_'.join(original_filename.split('_')[2:])
63
 
64
  try:
65
  # Handle MP3 files
66
  if isinstance(audio, mutagen.mp3.MP3):
67
  try:
68
+ # Try to get ID3 tags first
69
+ if audio.tags:
70
+ id3 = audio.tags
71
+ else:
72
+ # If no tags exist, try to create them
73
+ id3 = mutagen.id3.ID3(filepath)
74
+
75
+ # Get basic metadata
76
  metadata.update({
77
+ 'title': str(id3.get('TIT2', [''])[0]) if 'TIT2' in id3 else '',
78
+ 'artist': str(id3.get('TPE1', ['Unknown Artist'])[0]) if 'TPE1' in id3 else 'Unknown Artist',
79
+ 'album': str(id3.get('TALB', [''])[0]) if 'TALB' in id3 else '',
80
+ 'genre': str(id3.get('TCON', [''])[0]) if 'TCON' in id3 else '',
81
+ 'date': str(id3.get('TDRC', [''])[0]) if 'TDRC' in id3 else ''
82
  })
83
+
84
+ # Extract artwork - try different APIC frame keys
85
+ apic_keys = ['APIC:', 'APIC:Cover', 'APIC:Front Cover', 'APIC']
86
+ for key in apic_keys:
87
+ if key in id3:
88
+ artwork = id3[key]
89
+ if artwork and artwork.data:
90
+ logger.info(f"Found artwork in {key} frame")
91
+ metadata['artwork'] = {
92
+ 'mime': artwork.mime,
93
+ 'data': base64.b64encode(artwork.data).decode('utf-8')
94
+ }
95
+ break
96
+
97
+ if not metadata['artwork']:
98
+ logger.info("No artwork found in ID3 tags")
99
+
100
+ except Exception as e:
101
+ logger.error(f"Error reading ID3 tags: {str(e)}")
102
+ # Fallback to EasyID3 if ID3 fails
103
+ try:
104
+ easy_id3 = EasyID3(filepath)
105
+ metadata.update({
106
+ 'title': easy_id3.get('title', [''])[0],
107
+ 'artist': easy_id3.get('artist', ['Unknown Artist'])[0],
108
+ 'album': easy_id3.get('album', [''])[0],
109
+ 'genre': easy_id3.get('genre', [''])[0],
110
+ 'date': easy_id3.get('date', [''])[0]
111
+ })
112
+ except:
113
+ metadata['title'] = original_filename
114
+
115
  # Handle FLAC files
116
  elif isinstance(audio, mutagen.flac.FLAC):
117
  metadata.update({
 
121
  'genre': audio.tags.get('GENRE', [''])[0] if audio.tags and 'GENRE' in audio.tags else None,
122
  'date': audio.tags.get('DATE', [''])[0] if audio.tags and 'DATE' in audio.tags else None
123
  })
124
+
125
+ # Extract artwork from pictures
126
+ if audio.pictures:
127
+ picture = audio.pictures[0]
128
+ metadata['artwork'] = {
129
+ 'mime': picture.mime,
130
+ 'data': base64.b64encode(picture.data).decode('utf-8')
131
+ }
132
 
133
  # Handle OGG files
134
  elif isinstance(audio, mutagen.oggvorbis.OggVorbis):
 
139
  'genre': audio.get('genre', [''])[0] if 'genre' in audio else None,
140
  'date': audio.get('date', [''])[0] if 'date' in audio else None
141
  })
142
+
143
+ # Extract artwork if available (some OGG files store artwork in metadata)
144
+ if 'METADATA_BLOCK_PICTURE' in audio:
145
+ try:
146
+ picture_data = base64.b64decode(audio['METADATA_BLOCK_PICTURE'][0])
147
+ picture = mutagen.flac.Picture(picture_data)
148
+ metadata['artwork'] = {
149
+ 'mime': picture.mime,
150
+ 'data': base64.b64encode(picture.data).decode('utf-8')
151
+ }
152
+ except:
153
+ pass
154
 
155
  # If title is not found or empty, use original filename
156
  if not metadata['title']:
157
  metadata['title'] = original_filename
158
 
159
  except Exception as e:
160
+ logger.error(f"Error reading tags: {str(e)}")
161
  metadata['title'] = original_filename
162
 
163
  return metadata
164
  except Exception as e:
165
+ logger.error(f"Error extracting metadata: {str(e)}")
166
  return {
167
  'title': os.path.splitext(os.path.basename(filepath))[0].split('_', 2)[-1],
168
  'artist': 'Unknown Artist'
 
211
 
212
  @app.route('/upload', methods=['POST'])
213
  def upload_file():
214
+ logger.info('Upload request received')
215
+
216
  if 'files[]' not in request.files:
217
+ logger.warning('No files in request')
218
  return jsonify({'success': False, 'error': 'No files uploaded'}), 400
219
 
220
  files = request.files.getlist('files[]')
221
+ logger.info(f'Received {len(files)} files')
222
+
223
  if not files or all(file.filename == '' for file in files):
224
+ logger.warning('No selected files')
225
  return jsonify({'success': False, 'error': 'No selected files'}), 400
226
 
227
  if len(files) > app.config['MAX_FILES']:
228
+ logger.warning(f'Too many files: {len(files)}')
229
  return jsonify({
230
  'success': False,
231
  'error': f"Maximum {app.config['MAX_FILES']} files can be uploaded at once"
 
233
 
234
  results = []
235
  for file in files:
236
+ logger.info(f'Processing file: {file.filename}')
237
  if file and allowed_file(file.filename):
238
  try:
239
  filename = secure_filename(file.filename)
 
242
 
243
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
244
  file.save(filepath)
245
+ logger.info(f'File saved: {filepath}')
246
 
247
  file_timestamps[filename] = datetime.now()
248
 
249
  if not os.path.exists(filepath):
250
+ logger.error(f'Failed to save file: {filepath}')
251
  results.append({
252
  'filename': file.filename,
253
  'success': False,
 
255
  })
256
  continue
257
 
 
258
  metadata = extract_metadata(filepath)
259
+ logger.info(f'Metadata extracted: {metadata}')
260
 
261
  results.append({
262
  'filename': file.filename,
 
265
  'metadata': metadata
266
  })
267
  except Exception as e:
268
+ logger.error(f'Upload error for {file.filename}: {str(e)}')
269
  results.append({
270
  'filename': file.filename,
271
  'success': False,
272
  'error': 'Server error during upload'
273
  })
274
  else:
275
+ logger.warning(f'Invalid file type: {file.filename}')
276
  results.append({
277
  'filename': file.filename,
278
  'success': False,
279
  'error': 'Invalid file type'
280
  })
281
 
282
+ logger.info(f'Upload complete. Results: {results}')
 
 
283
  return jsonify({
284
  'success': True,
285
  'files': results
static/css/style.css CHANGED
@@ -440,14 +440,27 @@
440
  .track-artwork {
441
  width: 48px;
442
  height: 48px;
 
443
  border-radius: var(--radius-md);
444
  background: var(--surface-light);
445
  display: flex;
446
  align-items: center;
447
  justify-content: center;
 
 
 
 
 
 
448
  color: var(--text-secondary);
449
  }
450
 
 
 
 
 
 
 
451
  .music-controls {
452
  display: flex;
453
  align-items: center;
@@ -491,13 +504,18 @@
491
  bottom: 100%;
492
  left: 50%;
493
  transform: translateX(-50%);
 
 
494
  background: var(--surface-color);
495
- padding: 0.75rem;
496
  border-radius: var(--radius-md);
497
  box-shadow: var(--shadow-md);
498
  opacity: 0;
499
  visibility: hidden;
500
  transition: all var(--transition-fast);
 
 
 
501
  }
502
 
503
  .volume-control:hover .volume-slider-container {
@@ -506,6 +524,54 @@
506
  transform: translateX(-50%) translateY(-8px);
507
  }
508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  /* Keyboard shortcuts */
510
  .keyboard-shortcuts {
511
  position: fixed;
@@ -840,4 +906,241 @@ canvas {
840
  top: 0;
841
  left: 0;
842
  z-index: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  }
 
440
  .track-artwork {
441
  width: 48px;
442
  height: 48px;
443
+ min-width: 48px;
444
  border-radius: var(--radius-md);
445
  background: var(--surface-light);
446
  display: flex;
447
  align-items: center;
448
  justify-content: center;
449
+ overflow: hidden;
450
+ border: 1px solid var(--surface-light);
451
+ }
452
+
453
+ .track-artwork i {
454
+ font-size: 1.5rem;
455
  color: var(--text-secondary);
456
  }
457
 
458
+ .track-artwork img {
459
+ width: 100%;
460
+ height: 100%;
461
+ object-fit: cover;
462
+ }
463
+
464
  .music-controls {
465
  display: flex;
466
  align-items: center;
 
504
  bottom: 100%;
505
  left: 50%;
506
  transform: translateX(-50%);
507
+ width: 120px;
508
+ padding: 1rem;
509
  background: var(--surface-color);
510
+ border: 1px solid var(--surface-light);
511
  border-radius: var(--radius-md);
512
  box-shadow: var(--shadow-md);
513
  opacity: 0;
514
  visibility: hidden;
515
  transition: all var(--transition-fast);
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
  }
520
 
521
  .volume-control:hover .volume-slider-container {
 
524
  transform: translateX(-50%) translateY(-8px);
525
  }
526
 
527
+ /* Volume slider track */
528
+ .volume-slider-container .volume-track {
529
+ position: relative;
530
+ width: 100%;
531
+ height: 4px;
532
+ background: var(--surface-light);
533
+ border-radius: 2px;
534
+ overflow: hidden;
535
+ }
536
+
537
+ /* Volume progress bar */
538
+ .volume-progress {
539
+ position: absolute;
540
+ left: 0;
541
+ top: 0;
542
+ height: 100%;
543
+ background: var(--primary-color);
544
+ border-radius: 2px;
545
+ pointer-events: none;
546
+ }
547
+
548
+ /* Volume handle */
549
+ .volume-handle {
550
+ position: absolute;
551
+ top: 50%;
552
+ transform: translate(-50%, -50%);
553
+ width: 12px;
554
+ height: 12px;
555
+ background: var(--primary-color);
556
+ border: 2px solid white;
557
+ border-radius: 50%;
558
+ box-shadow: var(--shadow-sm);
559
+ pointer-events: none;
560
+ }
561
+
562
+ /* Volume input range styling */
563
+ #volume {
564
+ position: absolute;
565
+ top: 0;
566
+ left: 0;
567
+ width: 100%;
568
+ height: 100%;
569
+ opacity: 0;
570
+ cursor: pointer;
571
+ margin: 0;
572
+ padding: 0;
573
+ }
574
+
575
  /* Keyboard shortcuts */
576
  .keyboard-shortcuts {
577
  position: fixed;
 
906
  top: 0;
907
  left: 0;
908
  z-index: 1;
909
+ }
910
+
911
+ /* Add these styles for the no-tracks message */
912
+ .no-tracks-message {
913
+ display: flex;
914
+ flex-direction: column;
915
+ align-items: center;
916
+ justify-content: center;
917
+ padding: 2rem;
918
+ text-align: center;
919
+ color: var(--text-secondary);
920
+ }
921
+
922
+ .no-tracks-message i {
923
+ font-size: 2rem;
924
+ margin-bottom: 1rem;
925
+ }
926
+
927
+ .no-tracks-message p {
928
+ margin: 0 0 1rem 0;
929
+ }
930
+
931
+ .no-tracks-message .upload-btn {
932
+ padding: 0.5rem 1rem;
933
+ background: var(--primary-color);
934
+ color: white;
935
+ border: none;
936
+ border-radius: var(--radius-md);
937
+ cursor: pointer;
938
+ transition: all var(--transition-fast);
939
+ }
940
+
941
+ .no-tracks-message .upload-btn:hover {
942
+ background: var(--primary-hover);
943
+ transform: translateY(-2px);
944
+ }
945
+
946
+ /* Style the file input and upload button */
947
+ #audio-upload {
948
+ display: none; /* Hide the default input */
949
+ }
950
+
951
+ .upload-content .upload-btn,
952
+ .no-tracks-message .upload-btn {
953
+ padding: 0.75rem 1.5rem;
954
+ background: var(--primary-color);
955
+ color: white;
956
+ border: none;
957
+ border-radius: var(--radius-md);
958
+ cursor: pointer;
959
+ transition: all var(--transition-fast);
960
+ font-weight: 500;
961
+ display: flex;
962
+ align-items: center;
963
+ gap: 0.5rem;
964
+ }
965
+
966
+ .upload-content .upload-btn:hover,
967
+ .no-tracks-message .upload-btn:hover {
968
+ background: var(--primary-hover);
969
+ transform: translateY(-2px);
970
+ }
971
+
972
+ /* Fix dark mode control buttons */
973
+ [data-theme="dark"] .control-btn {
974
+ background: var(--surface-light);
975
+ color: var(--text-primary);
976
+ border: 1px solid rgba(255, 255, 255, 0.1);
977
+ }
978
+
979
+ [data-theme="dark"] .control-btn:hover:not(:disabled) {
980
+ background: rgba(255, 255, 255, 0.15);
981
+ border-color: rgba(255, 255, 255, 0.2);
982
+ }
983
+
984
+ [data-theme="dark"] .control-btn:disabled {
985
+ opacity: 0.5;
986
+ cursor: not-allowed;
987
+ }
988
+
989
+ /* Fix volume button styling */
990
+ .volume-btn {
991
+ width: 40px;
992
+ height: 40px;
993
+ border-radius: 50%;
994
+ background: var(--surface-light);
995
+ color: var(--text-primary);
996
+ border: 1px solid var(--surface-light);
997
+ transition: all var(--transition-fast);
998
+ display: flex;
999
+ align-items: center;
1000
+ justify-content: center;
1001
+ cursor: pointer;
1002
+ }
1003
+
1004
+ .volume-btn:hover {
1005
+ background: var(--surface-light);
1006
+ transform: translateY(-2px);
1007
+ }
1008
+
1009
+ /* Dark mode volume button */
1010
+ [data-theme="dark"] .volume-btn {
1011
+ background: var(--surface-light);
1012
+ color: var(--text-primary);
1013
+ border: 1px solid rgba(255, 255, 255, 0.1);
1014
+ }
1015
+
1016
+ [data-theme="dark"] .volume-btn:hover {
1017
+ background: rgba(255, 255, 255, 0.15);
1018
+ border-color: rgba(255, 255, 255, 0.2);
1019
+ }
1020
+
1021
+ /* Volume slider improvements */
1022
+ .volume-slider-container {
1023
+ width: 120px;
1024
+ padding: 1rem;
1025
+ background: var(--surface-color);
1026
+ border: 1px solid var(--surface-light);
1027
+ border-radius: var(--radius-md);
1028
+ box-shadow: var(--shadow-md);
1029
+ display: flex;
1030
+ align-items: center;
1031
+ gap: 0.5rem;
1032
+ }
1033
+
1034
+ /* Volume progress bar */
1035
+ .volume-progress {
1036
+ position: absolute;
1037
+ left: 0;
1038
+ top: 50%;
1039
+ transform: translateY(-50%);
1040
+ height: 4px;
1041
+ background: var(--primary-color);
1042
+ border-radius: 2px;
1043
+ pointer-events: none;
1044
+ }
1045
+
1046
+ /* Volume handle */
1047
+ .volume-handle {
1048
+ position: absolute;
1049
+ top: 50%;
1050
+ transform: translate(-50%, -50%);
1051
+ width: 12px;
1052
+ height: 12px;
1053
+ background: var(--primary-color);
1054
+ border: 2px solid white;
1055
+ border-radius: 50%;
1056
+ box-shadow: var(--shadow-sm);
1057
+ pointer-events: none;
1058
+ }
1059
+
1060
+ /* Volume input range styling */
1061
+ #volume {
1062
+ width: 100%;
1063
+ -webkit-appearance: none;
1064
+ background: transparent;
1065
+ cursor: pointer;
1066
+ }
1067
+
1068
+ #volume::-webkit-slider-runnable-track {
1069
+ width: 100%;
1070
+ height: 4px;
1071
+ background: var(--surface-light);
1072
+ border-radius: 2px;
1073
+ border: none;
1074
+ }
1075
+
1076
+ #volume::-webkit-slider-thumb {
1077
+ -webkit-appearance: none;
1078
+ height: 0;
1079
+ width: 0;
1080
+ }
1081
+
1082
+ #volume::-moz-range-track {
1083
+ width: 100%;
1084
+ height: 4px;
1085
+ background: var(--surface-light);
1086
+ border-radius: 2px;
1087
+ border: none;
1088
+ }
1089
+
1090
+ #volume::-moz-range-thumb {
1091
+ height: 0;
1092
+ width: 0;
1093
+ border: none;
1094
+ }
1095
+
1096
+ /* Playlist item artwork */
1097
+ .playlist-item .track-info {
1098
+ display: flex;
1099
+ align-items: center;
1100
+ gap: 1rem;
1101
+ width: 100%;
1102
+ }
1103
+
1104
+ .playlist-item .track-number {
1105
+ width: 24px;
1106
+ text-align: center;
1107
+ color: var(--text-secondary);
1108
+ font-size: 0.9rem;
1109
+ }
1110
+
1111
+ .playlist-item .track-artwork {
1112
+ width: 40px;
1113
+ height: 40px;
1114
+ min-width: 40px;
1115
+ }
1116
+
1117
+ /* Active track styling */
1118
+ .playlist-item.active .track-artwork {
1119
+ border-color: var(--primary-color);
1120
+ }
1121
+
1122
+ .playlist-item.active .track-artwork i {
1123
+ color: var(--primary-color);
1124
+ }
1125
+
1126
+ /* Now playing info artwork */
1127
+ .now-playing-info .track-artwork {
1128
+ width: 48px;
1129
+ height: 48px;
1130
+ min-width: 48px;
1131
+ border-radius: var(--radius-md);
1132
+ }
1133
+
1134
+ .now-playing-info .track-artwork i {
1135
+ font-size: 1.75rem;
1136
+ }
1137
+
1138
+ /* Dark mode adjustments */
1139
+ [data-theme="dark"] .track-artwork {
1140
+ border-color: rgba(255, 255, 255, 0.1);
1141
+ }
1142
+
1143
+ [data-theme="dark"] .playlist-item.active .track-artwork {
1144
+ border-color: var(--primary-color);
1145
+ background: rgba(76, 175, 80, 0.1);
1146
  }
static/js/main.js CHANGED
@@ -527,6 +527,7 @@ function setupOrbitControls() {
527
 
528
  // Enhanced file upload handling
529
  function setupUploadHandlers() {
 
530
  const uploadArea = document.getElementById('upload-area');
531
  const fileInput = document.getElementById('audio-upload');
532
  const uploadProgress = document.querySelector('.upload-progress');
@@ -539,107 +540,34 @@ function setupUploadHandlers() {
539
  return;
540
  }
541
 
 
542
  uploadArea.addEventListener('dragover', (e) => {
543
  e.preventDefault();
544
  uploadArea.classList.add('dragover');
 
545
  });
546
 
547
  uploadArea.addEventListener('dragleave', () => {
548
  uploadArea.classList.remove('dragover');
 
549
  });
550
 
551
  uploadArea.addEventListener('drop', (e) => {
552
  e.preventDefault();
553
  uploadArea.classList.remove('dragover');
 
554
  handleFiles(e.dataTransfer.files);
555
  });
556
 
557
  uploadArea.addEventListener('click', () => {
 
558
  fileInput.click();
559
  });
560
 
561
  fileInput.addEventListener('change', (e) => {
 
562
  handleFiles(e.target.files);
563
  });
564
-
565
- // Enhanced file upload with progress
566
- async function handleFiles(files) {
567
- if (!files || files.length === 0) {
568
- showError('No files selected');
569
- return;
570
- }
571
-
572
- if (!audioContext) {
573
- try {
574
- initAudio();
575
- } catch (error) {
576
- console.error('Failed to initialize audio:', error);
577
- showError('Failed to initialize audio system');
578
- return;
579
- }
580
- }
581
-
582
- const formData = new FormData();
583
- Array.from(files).forEach(file => {
584
- formData.append('files[]', file);
585
- });
586
-
587
- // Show upload progress if elements exist
588
- if (uploadProgress && progressFill && progressText) {
589
- uploadProgress.classList.add('visible');
590
- progressFill.style.width = '0%';
591
- progressText.textContent = '0%';
592
- }
593
-
594
- try {
595
- const response = await fetch('/upload', {
596
- method: 'POST',
597
- body: formData
598
- });
599
-
600
- if (!response.ok) {
601
- throw new Error(`HTTP error! status: ${response.status}`);
602
- }
603
-
604
- const data = await response.json();
605
-
606
- if (data.success) {
607
- showSuccess('Files uploaded successfully');
608
-
609
- // Filter successful uploads and create playlist
610
- const successfulFiles = data.files.filter(file => file.success);
611
- if (successfulFiles.length === 0) {
612
- showError('No files were uploaded successfully');
613
- return;
614
- }
615
-
616
- // Add originalIndex to each track
617
- playlist = successfulFiles.map((file, index) => ({
618
- name: file.filename,
619
- url: file.filepath,
620
- metadata: file.metadata,
621
- originalIndex: index
622
- }));
623
-
624
- createPlaylist();
625
- if (playlist.length > 0) {
626
- playTrack(0);
627
- }
628
- } else {
629
- showError(data.error || 'Upload failed');
630
- }
631
- } catch (error) {
632
- console.error('Upload error:', error);
633
- showError('Error uploading files');
634
- } finally {
635
- // Hide upload progress if elements exist
636
- if (uploadProgress && progressFill && progressText) {
637
- uploadProgress.classList.remove('visible');
638
- progressFill.style.width = '0%';
639
- progressText.textContent = '0%';
640
- }
641
- }
642
- }
643
  }
644
 
645
  // Enhanced toast notifications with null checks
@@ -1027,8 +955,25 @@ function updateVisualization(dataArray) {
1027
  // Create playlist UI
1028
  function createPlaylist() {
1029
  const tracksList = document.querySelector('.tracks-list');
 
 
 
1030
  tracksList.innerHTML = '';
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  playlist.forEach((track, index) => {
1033
  const metadata = track.metadata || {};
1034
  const duration = metadata.duration ? formatTime(metadata.duration) : '0:00';
@@ -1040,6 +985,12 @@ function createPlaylist() {
1040
  trackElement.innerHTML = `
1041
  <div class="track-info">
1042
  <span class="track-number">${index + 1}</span>
 
 
 
 
 
 
1043
  <div class="track-content">
1044
  <div class="track-title">
1045
  ${title}
@@ -1066,6 +1017,7 @@ function updateNowPlayingInfo() {
1066
 
1067
  const nowPlayingTitle = document.querySelector('.now-playing-title');
1068
  const nowPlayingArtist = document.querySelector('.now-playing-artist');
 
1069
 
1070
  if (nowPlayingTitle) {
1071
  nowPlayingTitle.textContent = metadata.title || track.name;
@@ -1078,6 +1030,12 @@ function updateNowPlayingInfo() {
1078
  }
1079
  nowPlayingArtist.textContent = artistInfo;
1080
  }
 
 
 
 
 
 
1081
  }
1082
  }
1083
 
@@ -1303,4 +1261,188 @@ function setupPlaylistControls() {
1303
  isRepeatActive = !isRepeatActive;
1304
  repeatBtn.classList.toggle('active', isRepeatActive);
1305
  });
1306
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  // Enhanced file upload handling
529
  function setupUploadHandlers() {
530
+ console.log('Setting up upload handlers...');
531
  const uploadArea = document.getElementById('upload-area');
532
  const fileInput = document.getElementById('audio-upload');
533
  const uploadProgress = document.querySelector('.upload-progress');
 
540
  return;
541
  }
542
 
543
+ // Add logging for drag and drop events
544
  uploadArea.addEventListener('dragover', (e) => {
545
  e.preventDefault();
546
  uploadArea.classList.add('dragover');
547
+ console.log('File being dragged over upload area');
548
  });
549
 
550
  uploadArea.addEventListener('dragleave', () => {
551
  uploadArea.classList.remove('dragover');
552
+ console.log('File drag left upload area');
553
  });
554
 
555
  uploadArea.addEventListener('drop', (e) => {
556
  e.preventDefault();
557
  uploadArea.classList.remove('dragover');
558
+ console.log('Files dropped:', e.dataTransfer.files.length, 'files');
559
  handleFiles(e.dataTransfer.files);
560
  });
561
 
562
  uploadArea.addEventListener('click', () => {
563
+ console.log('Upload area clicked, triggering file input');
564
  fileInput.click();
565
  });
566
 
567
  fileInput.addEventListener('change', (e) => {
568
+ console.log('Files selected:', e.target.files.length, 'files');
569
  handleFiles(e.target.files);
570
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
 
573
  // Enhanced toast notifications with null checks
 
955
  // Create playlist UI
956
  function createPlaylist() {
957
  const tracksList = document.querySelector('.tracks-list');
958
+ const noTracksMessage = document.querySelector('.no-tracks-message');
959
+
960
+ // Clear existing tracks
961
  tracksList.innerHTML = '';
962
 
963
+ if (!playlist || playlist.length === 0) {
964
+ // Show no tracks message if playlist is empty
965
+ if (noTracksMessage) {
966
+ noTracksMessage.style.display = 'flex';
967
+ }
968
+ return;
969
+ }
970
+
971
+ // Hide no tracks message if we have tracks
972
+ if (noTracksMessage) {
973
+ noTracksMessage.style.display = 'none';
974
+ }
975
+
976
+ // Create track elements
977
  playlist.forEach((track, index) => {
978
  const metadata = track.metadata || {};
979
  const duration = metadata.duration ? formatTime(metadata.duration) : '0:00';
 
985
  trackElement.innerHTML = `
986
  <div class="track-info">
987
  <span class="track-number">${index + 1}</span>
988
+ <div class="track-artwork">
989
+ ${metadata.artwork ?
990
+ `<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` :
991
+ '<i class="fas fa-music"></i>'
992
+ }
993
+ </div>
994
  <div class="track-content">
995
  <div class="track-title">
996
  ${title}
 
1017
 
1018
  const nowPlayingTitle = document.querySelector('.now-playing-title');
1019
  const nowPlayingArtist = document.querySelector('.now-playing-artist');
1020
+ const trackArtwork = document.querySelector('.now-playing-info .track-artwork');
1021
 
1022
  if (nowPlayingTitle) {
1023
  nowPlayingTitle.textContent = metadata.title || track.name;
 
1030
  }
1031
  nowPlayingArtist.textContent = artistInfo;
1032
  }
1033
+
1034
+ if (trackArtwork) {
1035
+ trackArtwork.innerHTML = metadata.artwork ?
1036
+ `<img src="data:${metadata.artwork.mime};base64,${metadata.artwork.data}" alt="Album artwork">` :
1037
+ '<i class="fas fa-music"></i>';
1038
+ }
1039
  }
1040
  }
1041
 
 
1261
  isRepeatActive = !isRepeatActive;
1262
  repeatBtn.classList.toggle('active', isRepeatActive);
1263
  });
1264
+ }
1265
+
1266
+ // Update handleFiles to append new tracks
1267
+ async function handleFiles(files) {
1268
+ console.log('Handling files:', files.length, 'files');
1269
+
1270
+ if (!files || files.length === 0) {
1271
+ console.warn('No files selected');
1272
+ showError('No files selected');
1273
+ return;
1274
+ }
1275
+
1276
+ if (!audioContext) {
1277
+ try {
1278
+ console.log('Initializing audio context...');
1279
+ initAudio();
1280
+ } catch (error) {
1281
+ console.error('Failed to initialize audio:', error);
1282
+ showError('Failed to initialize audio system');
1283
+ return;
1284
+ }
1285
+ }
1286
+
1287
+ const formData = new FormData();
1288
+ Array.from(files).forEach(file => {
1289
+ console.log('Adding file to upload:', file.name);
1290
+ formData.append('files[]', file);
1291
+ });
1292
+
1293
+ // Show upload progress
1294
+ const uploadProgress = document.querySelector('.upload-progress');
1295
+ const progressFill = uploadProgress?.querySelector('.progress-fill');
1296
+ const progressText = uploadProgress?.querySelector('.progress-text');
1297
+
1298
+ if (uploadProgress && progressFill && progressText) {
1299
+ uploadProgress.classList.add('visible');
1300
+ progressFill.style.width = '0%';
1301
+ progressText.textContent = '0%';
1302
+ }
1303
+
1304
+ try {
1305
+ console.log('Starting file upload...');
1306
+ const response = await fetch('/upload', {
1307
+ method: 'POST',
1308
+ body: formData
1309
+ });
1310
+
1311
+ if (!response.ok) {
1312
+ throw new Error(`HTTP error! status: ${response.status}`);
1313
+ }
1314
+
1315
+ const data = await response.json();
1316
+ console.log('Upload response:', data);
1317
+
1318
+ if (data.success) {
1319
+ console.log('Files uploaded successfully');
1320
+ showSuccess('Files uploaded successfully');
1321
+
1322
+ const successfulFiles = data.files.filter(file => file.success);
1323
+ console.log('Successful uploads:', successfulFiles.length, 'files');
1324
+
1325
+ if (successfulFiles.length === 0) {
1326
+ console.warn('No files were uploaded successfully');
1327
+ showError('No files were uploaded successfully');
1328
+ return;
1329
+ }
1330
+
1331
+ // Get the next index for new tracks
1332
+ const startIndex = playlist.length;
1333
+
1334
+ // Create new track objects
1335
+ const newTracks = successfulFiles.map((file, index) => ({
1336
+ name: file.filename,
1337
+ url: file.filepath,
1338
+ metadata: file.metadata,
1339
+ originalIndex: startIndex + index
1340
+ }));
1341
+
1342
+ // Append new tracks to existing playlist
1343
+ playlist = [...playlist, ...newTracks];
1344
+
1345
+ console.log('Updated playlist:', playlist);
1346
+ createPlaylist();
1347
+
1348
+ // Only start playing if nothing is currently playing
1349
+ if (playlist.length > 0 && !isPlaying) {
1350
+ console.log('Playing first track...');
1351
+ playTrack(0);
1352
+ }
1353
+
1354
+ // Enable player controls
1355
+ document.querySelectorAll('.control-btn').forEach(btn => {
1356
+ btn.disabled = false;
1357
+ });
1358
+
1359
+ // Highlight currently playing track if any
1360
+ if (currentTrackIndex >= 0) {
1361
+ document.querySelectorAll('.playlist-item').forEach((item, i) => {
1362
+ item.classList.toggle('active', i === currentTrackIndex);
1363
+ });
1364
+ }
1365
+ } else {
1366
+ console.error('Upload failed:', data.error);
1367
+ showError(data.error || 'Upload failed');
1368
+ }
1369
+ } catch (error) {
1370
+ console.error('Upload error:', error);
1371
+ showError('Error uploading files');
1372
+ } finally {
1373
+ if (uploadProgress) {
1374
+ uploadProgress.classList.remove('visible');
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ // Update volume control functions
1380
+ function setupVolumeControl() {
1381
+ const volumeBtn = document.querySelector('.volume-btn');
1382
+ const volumeSlider = document.getElementById('volume');
1383
+ const volumeProgress = document.querySelector('.volume-progress');
1384
+ const volumeHandle = document.querySelector('.volume-handle');
1385
+
1386
+ if (!volumeBtn || !volumeSlider || !volumeProgress || !volumeHandle) {
1387
+ console.error('Volume control elements not found');
1388
+ return;
1389
+ }
1390
+
1391
+ // Initialize volume
1392
+ let currentVolume = localStorage.getItem('volume') || 0.5;
1393
+ updateVolume(currentVolume);
1394
+
1395
+ // Update volume on slider change
1396
+ volumeSlider.addEventListener('input', (e) => {
1397
+ const value = parseFloat(e.target.value);
1398
+ updateVolume(value);
1399
+ });
1400
+
1401
+ // Update volume on button click (mute/unmute)
1402
+ volumeBtn.addEventListener('click', () => {
1403
+ if (audioElement) {
1404
+ if (audioElement.volume > 0) {
1405
+ // Store current volume before muting
1406
+ localStorage.setItem('previousVolume', audioElement.volume);
1407
+ updateVolume(0);
1408
+ } else {
1409
+ // Restore previous volume or default to 0.5
1410
+ const previousVolume = localStorage.getItem('previousVolume') || 0.5;
1411
+ updateVolume(previousVolume);
1412
+ }
1413
+ }
1414
+ });
1415
+
1416
+ function updateVolume(value) {
1417
+ // Update audio element
1418
+ if (audioElement) {
1419
+ audioElement.volume = value;
1420
+ }
1421
+
1422
+ // Update UI
1423
+ volumeProgress.style.width = `${value * 100}%`;
1424
+ volumeHandle.style.left = `${value * 100}%`;
1425
+ volumeSlider.value = value;
1426
+
1427
+ // Update button icon
1428
+ const icon = volumeBtn.querySelector('i');
1429
+ if (icon) {
1430
+ if (value === 0) {
1431
+ icon.className = 'fas fa-volume-mute';
1432
+ } else if (value < 0.5) {
1433
+ icon.className = 'fas fa-volume-down';
1434
+ } else {
1435
+ icon.className = 'fas fa-volume-up';
1436
+ }
1437
+ }
1438
+
1439
+ // Save volume to localStorage
1440
+ localStorage.setItem('volume', value);
1441
+ }
1442
+ }
1443
+
1444
+ // Add to initialization
1445
+ document.addEventListener('DOMContentLoaded', () => {
1446
+ // ... other initialization code ...
1447
+ setupVolumeControl();
1448
+ });
templates/index.html CHANGED
@@ -129,9 +129,11 @@
129
  <i class="fas fa-volume-up"></i>
130
  </button>
131
  <div class="volume-slider-container">
132
- <div class="volume-progress"></div>
133
- <div class="volume-handle"></div>
134
- <input type="range" id="volume" min="0" max="1" step="0.1" value="0.5">
 
 
135
  </div>
136
  </div>
137
  </div>
 
129
  <i class="fas fa-volume-up"></i>
130
  </button>
131
  <div class="volume-slider-container">
132
+ <div class="volume-track">
133
+ <div class="volume-progress"></div>
134
+ <div class="volume-handle"></div>
135
+ <input type="range" id="volume" min="0" max="1" step="0.01" value="0.5">
136
+ </div>
137
  </div>
138
  </div>
139
  </div>