Create index.html
Browse files- 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 |
+
==============================
|