htmlmusic / index.html
akborana4's picture
Create index.html
f274106 verified
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JioSaavn Mini Player • HF Space</title>
<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." />
<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" />
<style>
:root{
--bg: #0b0e14;
--panel: #111625;
--panel-2: #0f1421;
--text: #eef3fb;
--muted: #a9b4c7;
--brand: #00e5ff;
--brand-2: #6ce7ff;
--accent: #8a7dff;
--danger: #ff4976;
--good: #4ade80;
--warning: #f59e0b;
--shadow: 0 10px 30px rgba(0,0,0,.35);
--radius-xl: 18px;
--radius-lg: 14px;
--radius-md: 10px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
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";
color:var(--text); background: radial-gradient(1200px 800px at 100% -100%, #1a1e2a 10%, transparent 60%),
radial-gradient(900px 700px at -10% -100%, #091427 20%, transparent 70%),
linear-gradient(180deg, #0b0e14, #0b0e14);
display:flex; flex-direction:column; min-height:100%;
}
a{color:var(--brand)} a:hover{opacity:.9}
header{
position:sticky; top:0; z-index:40; backdrop-filter: blur(10px);
background: linear-gradient(180deg, rgba(17,22,37,.85), rgba(17,22,37,.65));
border-bottom:1px solid rgba(255,255,255,.06);
}
.wrap{max-width:1200px; margin:0 auto; padding:16px;}
.topbar{display:flex; gap:12px; align-items:center; justify-content:space-between}
.brand{display:flex; align-items:center; gap:10px; font-weight:800; letter-spacing:.2px}
.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}
.brand .logo svg{filter:drop-shadow(0 6px 16px rgba(0,229,255,.35))}.search{flex:1; display:flex; gap:10px; align-items:center}
.search input{
flex:1; border:none; outline:none; padding:14px 14px 14px 44px; color:var(--text);
background: linear-gradient(180deg, #0f1421, #0b0f1a); border:1px solid rgba(255,255,255,.07);
border-radius: var(--radius-lg); box-shadow: inset 0 0 0 1px rgba(255,255,255,.02);
}
.search .field{position:relative; flex:1}
.search .field svg{position:absolute; left:12px; top:50%; transform:translateY(-50%); opacity:.7}
.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}
.btn:active{transform:translateY(1px)}
.btn.alt{background:linear-gradient(180deg, #21283a, #171d2c); color:var(--text); border:1px solid rgba(255,255,255,.06)}
.content{display:grid; grid-template-columns: 320px 1fr; gap:16px; align-items:start; padding-bottom:140px}
@media (max-width: 1020px){ .content{ grid-template-columns: 1fr; } }
.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)}
/* Queue */
.queue{position:sticky; top:92px; max-height:calc(100dvh - 140px); overflow:auto}
.queue h3{margin:0; padding:14px 16px; font-size:14px; letter-spacing:.4px; text-transform:uppercase; opacity:.8}
.q-actions{display:flex; gap:8px; padding:0 12px 8px; flex-wrap:wrap}
.q-list{display:flex; flex-direction:column; gap:8px; padding:0 8px 12px}
.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}
.q-item:hover{background:rgba(255,255,255,.04); border-color: rgba(255,255,255,.06)}
.q-item.active{background:linear-gradient(180deg, rgba(108,231,255,.12), rgba(138,125,255,.1)); border-color:rgba(108,231,255,.35)}
.thumb{width:44px; height:44px; border-radius:10px; overflow:hidden}
.thumb img{width:100%; height:100%; object-fit:cover}
.meta{min-width:0}
.title{font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.artists{font-size:12px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.pill{font-size:11px; padding:3px 8px; border-radius:999px; border:1px solid rgba(255,255,255,.12); color:var(--muted)}
/* Results grid */
.results{display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:14px}
.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)}
.cover{position:relative; border-radius:14px; overflow:hidden; aspect-ratio:1/1}
.cover img{width:100%; height:100%; object-fit:cover; display:block}
.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)}
.card .info{margin-top:10px}
.card .title{font-weight:800}
.actions{display:flex; gap:8px; margin-top:10px}
.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}
.icon-btn:hover{border-color:rgba(255,255,255,.2)}
/* Now Playing */
.now-playing{padding:16px}
.np-wrap{display:grid; grid-template-columns: 180px 1fr; gap:18px}
@media (max-width:720px){ .np-wrap{ grid-template-columns: 1fr; } }
.np-art{border-radius:16px; overflow:hidden; aspect-ratio:1/1; background:linear-gradient(180deg, #1b2234, #101624)}
.np-art img{width:100%; height:100%; object-fit:cover}
.np-meta .np-title{font-size:20px; font-weight:900}
.np-meta .np-artist{color:var(--muted)}
.np-ctrls{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px}
.stack{display:flex; flex-direction:column; gap:10px}
.range{appearance:none; width:100%; height:6px; background:#0e1320; border-radius:999px; outline:none; border:1px solid rgba(255,255,255,.08)}
.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)}
.time{display:flex; justify-content:space-between; font-size:12px; color:var(--muted)}
/* Lyrics */
.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}
/* Player bar */
.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)}
.bar .wrap{display:grid; grid-template-columns: 1fr auto 1fr; align-items:center; gap:12px}
.bar .mini{display:flex; gap:10px; align-items:center; min-width:0}
.bar .mini .thumb{width:54px; height:54px; border-radius:12px}
.bar .mini .title{font-weight:800}
.bar .mini .artists{font-size:12px}
.center-ctrls{display:flex; align-items:center; justify-content:center; gap:8px}
.volume{display:flex; align-items:center; gap:8px; justify-content:flex-end}
footer{padding:12px; text-align:center; color:var(--muted); font-size:12px}
</style>
</head>
<body>
<header>
<div class="wrap topbar">
<div class="brand">
<div class="logo" aria-hidden="true">
<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>
</div>
<div>HF • JioSaavn Mini</div>
</div>
<div class="search">
<div class="field">
<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>
<input id="q" placeholder="Search songs, e.g. 'sanam'" value="sanam"/>
</div>
<button class="btn" id="go">Search</button>
<button class="btn alt" id="shuffleAll">Shuffle Results</button>
</div>
</div>
</header> <main class="wrap content">
<aside class="panel queue" aria-label="Queue">
<h3>Queue</h3>
<div class="q-actions">
<button class="btn alt" id="clearQueue">Clear</button>
<button class="btn alt" id="saveQueue">Save</button>
<button class="btn alt" id="loadQueue">Load</button>
</div>
<div id="queue" class="q-list"></div>
</aside><section>
<div class="panel now-playing">
<div class="np-wrap">
<div class="np-art" id="npArt"><img alt="Cover" id="npImg" src=""/></div>
<div class="np-meta">
<div class="np-title" id="npTitle">Nothing playing</div>
<div class="np-artist" id="npArtist"></div>
<div class="stack">
<input id="seek" class="range" type="range" min="0" max="100" value="0"/>
<div class="time"><span id="cur">0:00</span><span id="dur">0:00</span></div>
<div class="np-ctrls">
<button class="icon-btn" id="prev" title="Prev (P)">⏮️</button>
<button class="icon-btn" id="play" title="Play/Pause (Space)">▶️</button>
<button class="icon-btn" id="next" title="Next (N)">⏭️</button>
<button class="icon-btn" id="repeat" title="Repeat All / One / Off">🔁</button>
<button class="icon-btn" id="shuffle" title="Toggle Shuffle (S)">🔀</button>
<div class="volume">
<span>🔊</span>
<input id="vol" class="range" type="range" min="0" max="1" step="0.01" value="0.9" style="width:140px"/>
</div>
<button class="icon-btn" id="toggleLyrics" title="Toggle lyrics (L)">🎤</button>
<a class="icon-btn" id="openLink" href="#" target="_blank" rel="noopener" title="Open on JioSaavn">🔗</a>
</div>
<div class="lyrics" id="lyrics" hidden></div>
</div>
</div>
</div>
</div>
<div class="panel" style="padding:16px; margin-top:16px">
<h3 style="margin:0 0 10px 0; opacity:.8; text-transform:uppercase; letter-spacing:.4px">Results</h3>
<div id="results" class="results"></div>
</div>
</section>
</main> <div class="bar">
<div class="wrap">
<div class="mini">
<div class="thumb"><img id="barImg" alt=""></div>
<div>
<div class="title" id="barTitle"></div>
<div class="artists" id="barArtists"></div>
</div>
</div>
<div class="center-ctrls">
<button class="icon-btn" id="bPrev">⏮️</button>
<button class="icon-btn" id="bPlay">▶️</button>
<button class="icon-btn" id="bNext">⏭️</button>
</div>
<div class="volume">
<span>🔊</span>
<input id="bVol" class="range" type="range" min="0" max="1" step="0.01" value="0.9" style="width:200px"/>
</div>
</div>
</div> <footer>Unofficial demo. Streams are provided by JioSaavn CDN via the public API. Use for testing only.</footer><audio id="audio"></audio>
<script>
const API = 'https://jio-saavn-api-eta.vercel.app';
/*** State ***/
const state = {
results: [], // search results (track objects)
queue: [], // queued track objects
index: -1, // current index in queue
repeat: 'all', // 'all' | 'one' | 'off'
shuffle: false,
volume: parseFloat(localStorage.getItem('volume') || '0.9'),
};
/*** Helpers ***/
const $ = sel => document.querySelector(sel);
const $$ = sel => Array.from(document.querySelectorAll(sel));
const fmtTime = s => {
if (isNaN(s) || !isFinite(s)) return '0:00';
s = Math.max(0, Math.floor(s));
const m = Math.floor(s/60); const sec = (s%60).toString().padStart(2,'0');
return `${m}:${sec}`;
};
const pick = (o, keys) => keys.reduce((a,k)=>{ if (o && o[k] != null) a[k]=o[k]; return a; }, {});
/** Map raw API item to our Track shape **/
function mapTrack(it){
const url = it.media_url || it.media_preview_url || it.vlink || '';
const t = {
id: it.id || crypto.randomUUID(),
title: it.song || 'Unknown',
artists: it.primary_artists || it.singers || '',
image: it.image || '',
album: it.album || '',
year: it.year || '',
duration: Number(it.duration || 0),
url,
preview: it.media_preview_url || '',
vlink: it.vlink || '',
perma_url: it.perma_url ? (it.perma_url.startsWith('http')? it.perma_url : 'https://www.jiosaavn.com'+it.perma_url) : '#',
disabled: String(it.disabled||'false') === 'true',
rights: it.rights || null,
lyrics: it.lyrics || null,
};
return t;
}
/** UI Renderers **/
function renderResults(){
const root = $('#results');
root.innerHTML = '';
if (!state.results.length){ root.innerHTML = '<div style="opacity:.7">No results. Try another search.</div>'; return; }
for(const t of state.results){
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `
<div class=\"cover\"><img src=\"${t.image}\" alt=\"${t.title}\">${t.disabled?'\n <span class=\"badge\" title=\"Might require pro on Saavn\">PRO</span>':''}
</div>
<div class=\"info\">
<div class=\"title\">${t.title}</div>
<div class=\"artists\">${t.artists || '—'}</div>
</div>
<div class=\"actions\">
<button class=\"icon-btn\" title=\"Play now\">▶️</button>
<button class=\"icon-btn\" title=\"Add to queue\">➕</button>
</div>`;
const [playBtn, addBtn] = div.querySelectorAll('.icon-btn');
playBtn.onclick = () => { enqueue([t], true); };
addBtn.onclick = () => { enqueue([t], false); };
root.appendChild(div);
}
}
function renderQueue(){
const root = $('#queue'); root.innerHTML = '';
state.queue.forEach((t, i)=>{
const div = document.createElement('div');
div.className = 'q-item' + (i===state.index? ' active':'');
div.innerHTML = `
<div class=\"thumb\"><img src=\"${t.image}\" alt=\"${t.title}\"></div>
<div class=\"meta\"><div class=\"title\">${t.title}</div><div class=\"artists\">${t.artists || ''}</div></div>
<div style=\"display:flex; gap:6px\">
<span class=\"pill\">${t.year || ''}</span>
<button class=\"icon-btn\" title=\"Remove\">✖️</button>
</div>`;
div.onclick = (e)=>{ if (!(e.target instanceof HTMLButtonElement)) playAt(i); };
div.querySelector('button').onclick = (e)=>{ e.stopPropagation(); removeAt(i); };
root.appendChild(div);
});
}
function renderNow(){
const t = state.queue[state.index];
const has = !!t;
$('#npImg').src = has? t.image : '';
$('#barImg').src = has? t.image : '';
$('#npTitle').textContent = has? t.title : 'Nothing playing';
$('#barTitle').textContent = has? t.title : '—';
$('#npArtist').textContent = has? t.artists : '—';
$('#barArtists').textContent = has? t.artists : '—';
$('#openLink').href = has? t.perma_url : '#';
$('#lyrics').textContent = (has && t.lyrics) ? t.lyrics : (has? 'No lyrics found for this track.' : '');
}
/*** Audio Engine ***/
const audio = $('#audio');
audio.preload = 'metadata';
function setVolume(v){
state.volume = v;
audio.volume = v;
$('#vol').value = v; $('#bVol').value = v;
localStorage.setItem('volume', String(v));
}
async function playAt(i){
if (i < 0 || i >= state.queue.length) return;
state.index = i; renderQueue(); renderNow();
const t = state.queue[i];
const src = t.url || t.preview || t.vlink;
if(!src){ alert('No playable URL for this track.'); return; }
audio.src = src;
try{ await audio.play(); togglePlayButtons(true);}catch(e){ console.warn(e); togglePlayButtons(false); }
updateTitles('▶');
}
function togglePlayButtons(isPlaying){
$('#play').textContent = isPlaying? '⏸️':'▶️';
$('#bPlay').textContent = isPlaying? '⏸️':'▶️';
}
function next(){
if (!state.queue.length) return;
if (state.shuffle){
const n = Math.floor(Math.random()*state.queue.length);
playAt(n); return;
}
const last = state.index === state.queue.length-1;
if (last){
if (state.repeat === 'all') playAt(0);
else togglePlayButtons(false);
} else playAt(state.index+1);
}
function prev(){ if (audio.currentTime > 3) { audio.currentTime = 0; } else playAt(Math.max(0, state.index-1)); }
audio.addEventListener('timeupdate', ()=>{
$('#seek').value = (audio.currentTime / (audio.duration||1)) * 100;
$('#cur').textContent = fmtTime(audio.currentTime);
$('#dur').textContent = fmtTime(audio.duration);
});
audio.addEventListener('ended', ()=>{
if (state.repeat === 'one') { playAt(state.index); return; }
next();
});
audio.addEventListener('play', ()=> togglePlayButtons(true));
audio.addEventListener('pause', ()=> togglePlayButtons(false));
/*** Actions ***/
async function search(q){
updateTitles('⏳');
try{
const res = await fetch(`${API}/song/?query=${encodeURIComponent(q)}&lyrics=true`);
const data = await res.json();
state.results = Array.isArray(data) ? data.map(mapTrack) : [];
}catch(e){ console.error(e); state.results = []; }
renderResults(); updateTitles();
}
function enqueue(items, playNow=false){
const before = state.queue.length;
for(const it of items){ state.queue.push(it); }
persist();
renderQueue();
if (playNow) playAt(before); // play the first of newly added
}
function removeAt(i){ state.queue.splice(i,1); if (i <= state.index) state.index = Math.max(0, state.index-1); persist(); renderQueue(); }
function persist(){
const tiny = state.queue.map(t=>pick(t,['id','title','artists','image','album','year','duration','url','preview','vlink','perma_url','lyrics']));
localStorage.setItem('queue', JSON.stringify({queue: tiny, index: state.index}));
}
function restore(){
try{
const saved = JSON.parse(localStorage.getItem('queue')||'null');
if (saved && Array.isArray(saved.queue)){
state.queue = saved.queue; state.index = saved.index ?? -1;
}
}catch(e){}
}
function updateTitles(prefix=''){
document.title = `${prefix? prefix+' ':''}${state.queue[state.index]?.title || 'JioSaavn Mini Player • HF'}`;
}
/*** Wire UI ***/
$('#go').onclick = ()=> search($('#q').value.trim() || 'sanam');
$('#shuffleAll').onclick = ()=>{
if (!state.results.length) return;
const shuffled=[...state.results].sort(()=>Math.random()-.5);
enqueue(shuffled, true);
};
$('#clearQueue').onclick = ()=>{ state.queue=[]; state.index=-1; persist(); renderQueue(); renderNow(); };
$('#saveQueue').onclick = ()=>{ persist(); alert('Queue saved locally.'); };
$('#loadQueue').onclick = ()=>{ restore(); renderQueue(); if(state.index>=0) renderNow(); };
$('#play').onclick = ()=>{ if (audio.paused) audio.play(); else audio.pause(); };
$('#bPlay').onclick = ()=> $('#play').onclick();
$('#next').onclick = next; $('#bNext').onclick = next;
$('#prev').onclick = prev; $('#bPrev').onclick = prev;
$('#vol').oninput = e=> setVolume(parseFloat(e.target.value));
$('#bVol').oninput = e=> setVolume(parseFloat(e.target.value));
$('#seek').oninput = e=>{ const p = parseFloat(e.target.value)/100; audio.currentTime = p * (audio.duration||0); };
$('#shuffle').onclick = ()=>{ state.shuffle = !state.shuffle; $('#shuffle').style.filter = state.shuffle? 'drop-shadow(0 0 8px rgba(108,231,255,.8))':''; };
$('#repeat').onclick = ()=>{
state.repeat = state.repeat==='all'?'one': state.repeat==='one'?'off':'all';
$('#repeat').textContent = state.repeat==='one'?'🔂': state.repeat==='off'?'🔁❌':'🔁';
};
$('#toggleLyrics').onclick = ()=>{ const el=$('#lyrics'); el.hidden=!el.hidden; };
document.addEventListener('keydown', (e)=>{
if (['INPUT','TEXTAREA'].includes(e.target.tagName)) return;
if (e.code==='Space'){ e.preventDefault(); $('#play').onclick(); }
if (e.key==='n' || e.key==='N') next();
if (e.key==='p' || e.key==='P') prev();
if (e.key==='s' || e.key==='S') $('#shuffle').onclick();
if (e.key==='l' || e.key==='L') $('#toggleLyrics').onclick();
if (e.key==='ArrowLeft') audio.currentTime = Math.max(0, audio.currentTime-5);
if (e.key==='ArrowRight') audio.currentTime = Math.min(audio.duration||0, audio.currentTime+5);
if (e.key==='ArrowUp') setVolume(Math.min(1, state.volume+0.05));
if (e.key==='ArrowDown') setVolume(Math.max(0, state.volume-0.05));
});
/*** Init ***/
restore();
setVolume(state.volume);
renderQueue();
if (state.index>=0 && state.queue[state.index]) { renderNow(); }
// initial search
search($('#q').value);
</script></body>
</html><!-- ==========================
DOCKERFILE (save as: Dockerfile)
==============================