akborana4 commited on
Commit
f274106
·
verified ·
1 Parent(s): feabbed

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +452 -0
index.html ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html><html lang="en">
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ <title>JioSaavn Mini Player • HF Space</title>
6
+ <meta name="description" content="A sleek web music player powered by the JioSaavn Unofficial API. Search, queue, play, shuffle, and view lyrics in one page." />
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Ccircle cx='128' cy='128' r='120' fill='%2300e5ff'/%3E%3Cpath d='M88 76v104a40 40 0 1 0 24-37.3V84h56V68H88z' fill='white'/%3E%3C/svg%3E" />
8
+ <style>
9
+ :root{
10
+ --bg: #0b0e14;
11
+ --panel: #111625;
12
+ --panel-2: #0f1421;
13
+ --text: #eef3fb;
14
+ --muted: #a9b4c7;
15
+ --brand: #00e5ff;
16
+ --brand-2: #6ce7ff;
17
+ --accent: #8a7dff;
18
+ --danger: #ff4976;
19
+ --good: #4ade80;
20
+ --warning: #f59e0b;
21
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
22
+ --radius-xl: 18px;
23
+ --radius-lg: 14px;
24
+ --radius-md: 10px;
25
+ }
26
+ *{box-sizing:border-box}
27
+ html,body{height:100%}
28
+ body{
29
+ margin:0; font: 15px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
30
+ color:var(--text); background: radial-gradient(1200px 800px at 100% -100%, #1a1e2a 10%, transparent 60%),
31
+ radial-gradient(900px 700px at -10% -100%, #091427 20%, transparent 70%),
32
+ linear-gradient(180deg, #0b0e14, #0b0e14);
33
+ display:flex; flex-direction:column; min-height:100%;
34
+ }
35
+ a{color:var(--brand)} a:hover{opacity:.9}
36
+ header{
37
+ position:sticky; top:0; z-index:40; backdrop-filter: blur(10px);
38
+ background: linear-gradient(180deg, rgba(17,22,37,.85), rgba(17,22,37,.65));
39
+ border-bottom:1px solid rgba(255,255,255,.06);
40
+ }
41
+ .wrap{max-width:1200px; margin:0 auto; padding:16px;}
42
+ .topbar{display:flex; gap:12px; align-items:center; justify-content:space-between}
43
+ .brand{display:flex; align-items:center; gap:10px; font-weight:800; letter-spacing:.2px}
44
+ .brand .logo{width:36px; height:36px; border-radius:50%; box-shadow: var(--shadow); background: linear-gradient(120deg, var(--brand), var(--accent)); display:grid; place-items:center}
45
+ .brand .logo svg{filter:drop-shadow(0 6px 16px rgba(0,229,255,.35))}.search{flex:1; display:flex; gap:10px; align-items:center}
46
+ .search input{
47
+ flex:1; border:none; outline:none; padding:14px 14px 14px 44px; color:var(--text);
48
+ background: linear-gradient(180deg, #0f1421, #0b0f1a); border:1px solid rgba(255,255,255,.07);
49
+ border-radius: var(--radius-lg); box-shadow: inset 0 0 0 1px rgba(255,255,255,.02);
50
+ }
51
+ .search .field{position:relative; flex:1}
52
+ .search .field svg{position:absolute; left:12px; top:50%; transform:translateY(-50%); opacity:.7}
53
+ .btn{background: linear-gradient(180deg, var(--brand), var(--brand-2)); color:#00212a; border:none; padding:12px 16px; border-radius: var(--radius-lg); font-weight:700; box-shadow:0 8px 24px rgba(108,231,255,.25); cursor:pointer}
54
+ .btn:active{transform:translateY(1px)}
55
+ .btn.alt{background:linear-gradient(180deg, #21283a, #171d2c); color:var(--text); border:1px solid rgba(255,255,255,.06)}
56
+
57
+ .content{display:grid; grid-template-columns: 320px 1fr; gap:16px; align-items:start; padding-bottom:140px}
58
+ @media (max-width: 1020px){ .content{ grid-template-columns: 1fr; } }
59
+
60
+ .panel{background: linear-gradient(180deg, var(--panel), var(--panel-2)); border:1px solid rgba(255,255,255,.06); border-radius: var(--radius-xl); box-shadow: var(--shadow)}
61
+
62
+ /* Queue */
63
+ .queue{position:sticky; top:92px; max-height:calc(100dvh - 140px); overflow:auto}
64
+ .queue h3{margin:0; padding:14px 16px; font-size:14px; letter-spacing:.4px; text-transform:uppercase; opacity:.8}
65
+ .q-actions{display:flex; gap:8px; padding:0 12px 8px; flex-wrap:wrap}
66
+ .q-list{display:flex; flex-direction:column; gap:8px; padding:0 8px 12px}
67
+ .q-item{display:grid; grid-template-columns: 44px 1fr auto; gap:10px; align-items:center; padding:8px; border-radius:12px; cursor:pointer; border:1px solid transparent}
68
+ .q-item:hover{background:rgba(255,255,255,.04); border-color: rgba(255,255,255,.06)}
69
+ .q-item.active{background:linear-gradient(180deg, rgba(108,231,255,.12), rgba(138,125,255,.1)); border-color:rgba(108,231,255,.35)}
70
+ .thumb{width:44px; height:44px; border-radius:10px; overflow:hidden}
71
+ .thumb img{width:100%; height:100%; object-fit:cover}
72
+ .meta{min-width:0}
73
+ .title{font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
74
+ .artists{font-size:12px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
75
+ .pill{font-size:11px; padding:3px 8px; border-radius:999px; border:1px solid rgba(255,255,255,.12); color:var(--muted)}
76
+
77
+ /* Results grid */
78
+ .results{display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:14px}
79
+ .card{position:relative; padding:12px; border-radius:18px; border:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01)); box-shadow: var(--shadow)}
80
+ .cover{position:relative; border-radius:14px; overflow:hidden; aspect-ratio:1/1}
81
+ .cover img{width:100%; height:100%; object-fit:cover; display:block}
82
+ .badge{position:absolute; right:8px; top:8px; font-size:11px; padding:4px 8px; border-radius:999px; background:rgba(0,229,255,.12); border:1px solid rgba(0,229,255,.3); color:var(--brand)}
83
+ .card .info{margin-top:10px}
84
+ .card .title{font-weight:800}
85
+ .actions{display:flex; gap:8px; margin-top:10px}
86
+ .icon-btn{display:inline-grid; place-items:center; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg, #1b2234, #13192a); border-radius:12px; padding:8px; cursor:pointer}
87
+ .icon-btn:hover{border-color:rgba(255,255,255,.2)}
88
+
89
+ /* Now Playing */
90
+ .now-playing{padding:16px}
91
+ .np-wrap{display:grid; grid-template-columns: 180px 1fr; gap:18px}
92
+ @media (max-width:720px){ .np-wrap{ grid-template-columns: 1fr; } }
93
+ .np-art{border-radius:16px; overflow:hidden; aspect-ratio:1/1; background:linear-gradient(180deg, #1b2234, #101624)}
94
+ .np-art img{width:100%; height:100%; object-fit:cover}
95
+ .np-meta .np-title{font-size:20px; font-weight:900}
96
+ .np-meta .np-artist{color:var(--muted)}
97
+ .np-ctrls{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px}
98
+ .stack{display:flex; flex-direction:column; gap:10px}
99
+ .range{appearance:none; width:100%; height:6px; background:#0e1320; border-radius:999px; outline:none; border:1px solid rgba(255,255,255,.08)}
100
+ .range::-webkit-slider-thumb{appearance:none; width:14px; height:14px; border-radius:50%; background:var(--brand); box-shadow:0 0 0 6px rgba(0,229,255,.15)}
101
+ .time{display:flex; justify-content:space-between; font-size:12px; color:var(--muted)}
102
+
103
+ /* Lyrics */
104
+ .lyrics{margin-top:8px; padding:12px; border-radius:12px; background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.02)); border:1px solid rgba(255,255,255,.06); max-height:220px; overflow:auto; white-space:pre-wrap}
105
+
106
+ /* Player bar */
107
+ .bar{position:fixed; left:0; right:0; bottom:0; z-index:50; background:linear-gradient(180deg, rgba(15,20,33,.85), rgba(15,20,33,.95)); border-top:1px solid rgba(255,255,255,.07); backdrop-filter:blur(10px)}
108
+ .bar .wrap{display:grid; grid-template-columns: 1fr auto 1fr; align-items:center; gap:12px}
109
+ .bar .mini{display:flex; gap:10px; align-items:center; min-width:0}
110
+ .bar .mini .thumb{width:54px; height:54px; border-radius:12px}
111
+ .bar .mini .title{font-weight:800}
112
+ .bar .mini .artists{font-size:12px}
113
+ .center-ctrls{display:flex; align-items:center; justify-content:center; gap:8px}
114
+ .volume{display:flex; align-items:center; gap:8px; justify-content:flex-end}
115
+
116
+ footer{padding:12px; text-align:center; color:var(--muted); font-size:12px}
117
+
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <header>
122
+ <div class="wrap topbar">
123
+ <div class="brand">
124
+ <div class="logo" aria-hidden="true">
125
+ <svg width="22" height="22" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="128" cy="128" r="120" fill="#00E5FF"/><path d="M88 76v104a40 40 0 1 0 24-37.3V84h56V68H88z" fill="#fff"/></svg>
126
+ </div>
127
+ <div>HF • JioSaavn Mini</div>
128
+ </div>
129
+ <div class="search">
130
+ <div class="field">
131
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 21l-3.9-3.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="10" cy="10" r="6" stroke="currentColor" stroke-width="2"/></svg>
132
+ <input id="q" placeholder="Search songs, e.g. 'sanam'" value="sanam"/>
133
+ </div>
134
+ <button class="btn" id="go">Search</button>
135
+ <button class="btn alt" id="shuffleAll">Shuffle Results</button>
136
+ </div>
137
+ </div>
138
+ </header> <main class="wrap content">
139
+ <aside class="panel queue" aria-label="Queue">
140
+ <h3>Queue</h3>
141
+ <div class="q-actions">
142
+ <button class="btn alt" id="clearQueue">Clear</button>
143
+ <button class="btn alt" id="saveQueue">Save</button>
144
+ <button class="btn alt" id="loadQueue">Load</button>
145
+ </div>
146
+ <div id="queue" class="q-list"></div>
147
+ </aside><section>
148
+ <div class="panel now-playing">
149
+ <div class="np-wrap">
150
+ <div class="np-art" id="npArt"><img alt="Cover" id="npImg" src=""/></div>
151
+ <div class="np-meta">
152
+ <div class="np-title" id="npTitle">Nothing playing</div>
153
+ <div class="np-artist" id="npArtist">—</div>
154
+ <div class="stack">
155
+ <input id="seek" class="range" type="range" min="0" max="100" value="0"/>
156
+ <div class="time"><span id="cur">0:00</span><span id="dur">0:00</span></div>
157
+ <div class="np-ctrls">
158
+ <button class="icon-btn" id="prev" title="Prev (P)">⏮️</button>
159
+ <button class="icon-btn" id="play" title="Play/Pause (Space)">▶️</button>
160
+ <button class="icon-btn" id="next" title="Next (N)">⏭️</button>
161
+ <button class="icon-btn" id="repeat" title="Repeat All / One / Off">🔁</button>
162
+ <button class="icon-btn" id="shuffle" title="Toggle Shuffle (S)">🔀</button>
163
+ <div class="volume">
164
+ <span>🔊</span>
165
+ <input id="vol" class="range" type="range" min="0" max="1" step="0.01" value="0.9" style="width:140px"/>
166
+ </div>
167
+ <button class="icon-btn" id="toggleLyrics" title="Toggle lyrics (L)">🎤</button>
168
+ <a class="icon-btn" id="openLink" href="#" target="_blank" rel="noopener" title="Open on JioSaavn">🔗</a>
169
+ </div>
170
+ <div class="lyrics" id="lyrics" hidden></div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <div class="panel" style="padding:16px; margin-top:16px">
177
+ <h3 style="margin:0 0 10px 0; opacity:.8; text-transform:uppercase; letter-spacing:.4px">Results</h3>
178
+ <div id="results" class="results"></div>
179
+ </div>
180
+ </section>
181
+
182
+ </main> <div class="bar">
183
+ <div class="wrap">
184
+ <div class="mini">
185
+ <div class="thumb"><img id="barImg" alt=""></div>
186
+ <div>
187
+ <div class="title" id="barTitle">—</div>
188
+ <div class="artists" id="barArtists">—</div>
189
+ </div>
190
+ </div>
191
+ <div class="center-ctrls">
192
+ <button class="icon-btn" id="bPrev">⏮️</button>
193
+ <button class="icon-btn" id="bPlay">▶️</button>
194
+ <button class="icon-btn" id="bNext">⏭️</button>
195
+ </div>
196
+ <div class="volume">
197
+ <span>🔊</span>
198
+ <input id="bVol" class="range" type="range" min="0" max="1" step="0.01" value="0.9" style="width:200px"/>
199
+ </div>
200
+ </div>
201
+ </div> <footer>Unofficial demo. Streams are provided by JioSaavn CDN via the public API. Use for testing only.</footer><audio id="audio"></audio>
202
+
203
+ <script>
204
+ const API = 'https://jio-saavn-api-eta.vercel.app';
205
+
206
+ /*** State ***/
207
+ const state = {
208
+ results: [], // search results (track objects)
209
+ queue: [], // queued track objects
210
+ index: -1, // current index in queue
211
+ repeat: 'all', // 'all' | 'one' | 'off'
212
+ shuffle: false,
213
+ volume: parseFloat(localStorage.getItem('volume') || '0.9'),
214
+ };
215
+
216
+ /*** Helpers ***/
217
+ const $ = sel => document.querySelector(sel);
218
+ const $$ = sel => Array.from(document.querySelectorAll(sel));
219
+ const fmtTime = s => {
220
+ if (isNaN(s) || !isFinite(s)) return '0:00';
221
+ s = Math.max(0, Math.floor(s));
222
+ const m = Math.floor(s/60); const sec = (s%60).toString().padStart(2,'0');
223
+ return `${m}:${sec}`;
224
+ };
225
+
226
+ const pick = (o, keys) => keys.reduce((a,k)=>{ if (o && o[k] != null) a[k]=o[k]; return a; }, {});
227
+
228
+ /** Map raw API item to our Track shape **/
229
+ function mapTrack(it){
230
+ const url = it.media_url || it.media_preview_url || it.vlink || '';
231
+ const t = {
232
+ id: it.id || crypto.randomUUID(),
233
+ title: it.song || 'Unknown',
234
+ artists: it.primary_artists || it.singers || '',
235
+ image: it.image || '',
236
+ album: it.album || '',
237
+ year: it.year || '',
238
+ duration: Number(it.duration || 0),
239
+ url,
240
+ preview: it.media_preview_url || '',
241
+ vlink: it.vlink || '',
242
+ perma_url: it.perma_url ? (it.perma_url.startsWith('http')? it.perma_url : 'https://www.jiosaavn.com'+it.perma_url) : '#',
243
+ disabled: String(it.disabled||'false') === 'true',
244
+ rights: it.rights || null,
245
+ lyrics: it.lyrics || null,
246
+ };
247
+ return t;
248
+ }
249
+
250
+ /** UI Renderers **/
251
+ function renderResults(){
252
+ const root = $('#results');
253
+ root.innerHTML = '';
254
+ if (!state.results.length){ root.innerHTML = '<div style="opacity:.7">No results. Try another search.</div>'; return; }
255
+ for(const t of state.results){
256
+ const div = document.createElement('div');
257
+ div.className = 'card';
258
+ div.innerHTML = `
259
+ <div class=\"cover\"><img src=\"${t.image}\" alt=\"${t.title}\">${t.disabled?'\n <span class=\"badge\" title=\"Might require pro on Saavn\">PRO</span>':''}
260
+ </div>
261
+ <div class=\"info\">
262
+ <div class=\"title\">${t.title}</div>
263
+ <div class=\"artists\">${t.artists || '—'}</div>
264
+ </div>
265
+ <div class=\"actions\">
266
+ <button class=\"icon-btn\" title=\"Play now\">▶️</button>
267
+ <button class=\"icon-btn\" title=\"Add to queue\">➕</button>
268
+ </div>`;
269
+ const [playBtn, addBtn] = div.querySelectorAll('.icon-btn');
270
+ playBtn.onclick = () => { enqueue([t], true); };
271
+ addBtn.onclick = () => { enqueue([t], false); };
272
+ root.appendChild(div);
273
+ }
274
+ }
275
+
276
+ function renderQueue(){
277
+ const root = $('#queue'); root.innerHTML = '';
278
+ state.queue.forEach((t, i)=>{
279
+ const div = document.createElement('div');
280
+ div.className = 'q-item' + (i===state.index? ' active':'');
281
+ div.innerHTML = `
282
+ <div class=\"thumb\"><img src=\"${t.image}\" alt=\"${t.title}\"></div>
283
+ <div class=\"meta\"><div class=\"title\">${t.title}</div><div class=\"artists\">${t.artists || ''}</div></div>
284
+ <div style=\"display:flex; gap:6px\">
285
+ <span class=\"pill\">${t.year || ''}</span>
286
+ <button class=\"icon-btn\" title=\"Remove\">✖️</button>
287
+ </div>`;
288
+ div.onclick = (e)=>{ if (!(e.target instanceof HTMLButtonElement)) playAt(i); };
289
+ div.querySelector('button').onclick = (e)=>{ e.stopPropagation(); removeAt(i); };
290
+ root.appendChild(div);
291
+ });
292
+ }
293
+
294
+ function renderNow(){
295
+ const t = state.queue[state.index];
296
+ const has = !!t;
297
+ $('#npImg').src = has? t.image : '';
298
+ $('#barImg').src = has? t.image : '';
299
+ $('#npTitle').textContent = has? t.title : 'Nothing playing';
300
+ $('#barTitle').textContent = has? t.title : '—';
301
+ $('#npArtist').textContent = has? t.artists : '—';
302
+ $('#barArtists').textContent = has? t.artists : '—';
303
+ $('#openLink').href = has? t.perma_url : '#';
304
+ $('#lyrics').textContent = (has && t.lyrics) ? t.lyrics : (has? 'No lyrics found for this track.' : '');
305
+ }
306
+
307
+ /*** Audio Engine ***/
308
+ const audio = $('#audio');
309
+ audio.preload = 'metadata';
310
+
311
+ function setVolume(v){
312
+ state.volume = v;
313
+ audio.volume = v;
314
+ $('#vol').value = v; $('#bVol').value = v;
315
+ localStorage.setItem('volume', String(v));
316
+ }
317
+
318
+ async function playAt(i){
319
+ if (i < 0 || i >= state.queue.length) return;
320
+ state.index = i; renderQueue(); renderNow();
321
+ const t = state.queue[i];
322
+ const src = t.url || t.preview || t.vlink;
323
+ if(!src){ alert('No playable URL for this track.'); return; }
324
+ audio.src = src;
325
+ try{ await audio.play(); togglePlayButtons(true);}catch(e){ console.warn(e); togglePlayButtons(false); }
326
+ updateTitles('▶');
327
+ }
328
+
329
+ function togglePlayButtons(isPlaying){
330
+ $('#play').textContent = isPlaying? '⏸️':'▶️';
331
+ $('#bPlay').textContent = isPlaying? '⏸️':'▶️';
332
+ }
333
+
334
+ function next(){
335
+ if (!state.queue.length) return;
336
+ if (state.shuffle){
337
+ const n = Math.floor(Math.random()*state.queue.length);
338
+ playAt(n); return;
339
+ }
340
+ const last = state.index === state.queue.length-1;
341
+ if (last){
342
+ if (state.repeat === 'all') playAt(0);
343
+ else togglePlayButtons(false);
344
+ } else playAt(state.index+1);
345
+ }
346
+ function prev(){ if (audio.currentTime > 3) { audio.currentTime = 0; } else playAt(Math.max(0, state.index-1)); }
347
+
348
+ audio.addEventListener('timeupdate', ()=>{
349
+ $('#seek').value = (audio.currentTime / (audio.duration||1)) * 100;
350
+ $('#cur').textContent = fmtTime(audio.currentTime);
351
+ $('#dur').textContent = fmtTime(audio.duration);
352
+ });
353
+ audio.addEventListener('ended', ()=>{
354
+ if (state.repeat === 'one') { playAt(state.index); return; }
355
+ next();
356
+ });
357
+ audio.addEventListener('play', ()=> togglePlayButtons(true));
358
+ audio.addEventListener('pause', ()=> togglePlayButtons(false));
359
+
360
+ /*** Actions ***/
361
+ async function search(q){
362
+ updateTitles('⏳');
363
+ try{
364
+ const res = await fetch(`${API}/song/?query=${encodeURIComponent(q)}&lyrics=true`);
365
+ const data = await res.json();
366
+ state.results = Array.isArray(data) ? data.map(mapTrack) : [];
367
+ }catch(e){ console.error(e); state.results = []; }
368
+ renderResults(); updateTitles();
369
+ }
370
+
371
+ function enqueue(items, playNow=false){
372
+ const before = state.queue.length;
373
+ for(const it of items){ state.queue.push(it); }
374
+ persist();
375
+ renderQueue();
376
+ if (playNow) playAt(before); // play the first of newly added
377
+ }
378
+
379
+ function removeAt(i){ state.queue.splice(i,1); if (i <= state.index) state.index = Math.max(0, state.index-1); persist(); renderQueue(); }
380
+
381
+ function persist(){
382
+ const tiny = state.queue.map(t=>pick(t,['id','title','artists','image','album','year','duration','url','preview','vlink','perma_url','lyrics']));
383
+ localStorage.setItem('queue', JSON.stringify({queue: tiny, index: state.index}));
384
+ }
385
+
386
+ function restore(){
387
+ try{
388
+ const saved = JSON.parse(localStorage.getItem('queue')||'null');
389
+ if (saved && Array.isArray(saved.queue)){
390
+ state.queue = saved.queue; state.index = saved.index ?? -1;
391
+ }
392
+ }catch(e){}
393
+ }
394
+
395
+ function updateTitles(prefix=''){
396
+ document.title = `${prefix? prefix+' ':''}${state.queue[state.index]?.title || 'JioSaavn Mini Player • HF'}`;
397
+ }
398
+
399
+ /*** Wire UI ***/
400
+ $('#go').onclick = ()=> search($('#q').value.trim() || 'sanam');
401
+ $('#shuffleAll').onclick = ()=>{
402
+ if (!state.results.length) return;
403
+ const shuffled=[...state.results].sort(()=>Math.random()-.5);
404
+ enqueue(shuffled, true);
405
+ };
406
+
407
+ $('#clearQueue').onclick = ()=>{ state.queue=[]; state.index=-1; persist(); renderQueue(); renderNow(); };
408
+ $('#saveQueue').onclick = ()=>{ persist(); alert('Queue saved locally.'); };
409
+ $('#loadQueue').onclick = ()=>{ restore(); renderQueue(); if(state.index>=0) renderNow(); };
410
+
411
+ $('#play').onclick = ()=>{ if (audio.paused) audio.play(); else audio.pause(); };
412
+ $('#bPlay').onclick = ()=> $('#play').onclick();
413
+ $('#next').onclick = next; $('#bNext').onclick = next;
414
+ $('#prev').onclick = prev; $('#bPrev').onclick = prev;
415
+
416
+ $('#vol').oninput = e=> setVolume(parseFloat(e.target.value));
417
+ $('#bVol').oninput = e=> setVolume(parseFloat(e.target.value));
418
+
419
+ $('#seek').oninput = e=>{ const p = parseFloat(e.target.value)/100; audio.currentTime = p * (audio.duration||0); };
420
+
421
+ $('#shuffle').onclick = ()=>{ state.shuffle = !state.shuffle; $('#shuffle').style.filter = state.shuffle? 'drop-shadow(0 0 8px rgba(108,231,255,.8))':''; };
422
+ $('#repeat').onclick = ()=>{
423
+ state.repeat = state.repeat==='all'?'one': state.repeat==='one'?'off':'all';
424
+ $('#repeat').textContent = state.repeat==='one'?'🔂': state.repeat==='off'?'🔁❌':'🔁';
425
+ };
426
+
427
+ $('#toggleLyrics').onclick = ()=>{ const el=$('#lyrics'); el.hidden=!el.hidden; };
428
+
429
+ document.addEventListener('keydown', (e)=>{
430
+ if (['INPUT','TEXTAREA'].includes(e.target.tagName)) return;
431
+ if (e.code==='Space'){ e.preventDefault(); $('#play').onclick(); }
432
+ if (e.key==='n' || e.key==='N') next();
433
+ if (e.key==='p' || e.key==='P') prev();
434
+ if (e.key==='s' || e.key==='S') $('#shuffle').onclick();
435
+ if (e.key==='l' || e.key==='L') $('#toggleLyrics').onclick();
436
+ if (e.key==='ArrowLeft') audio.currentTime = Math.max(0, audio.currentTime-5);
437
+ if (e.key==='ArrowRight') audio.currentTime = Math.min(audio.duration||0, audio.currentTime+5);
438
+ if (e.key==='ArrowUp') setVolume(Math.min(1, state.volume+0.05));
439
+ if (e.key==='ArrowDown') setVolume(Math.max(0, state.volume-0.05));
440
+ });
441
+
442
+ /*** Init ***/
443
+ restore();
444
+ setVolume(state.volume);
445
+ renderQueue();
446
+ if (state.index>=0 && state.queue[state.index]) { renderNow(); }
447
+ // initial search
448
+ search($('#q').value);
449
+ </script></body>
450
+ </html><!-- ==========================
451
+ DOCKERFILE (save as: Dockerfile)
452
+ ==============================