Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Music Stream Player</title>
<style>
:root {
--bg: #0f1220;
--panel: rgba(255,255,255,0.08);
--panel-2: rgba(255,255,255,0.12);
--text: #f4f7fb;
--muted: rgba(244,247,251,0.7);
--accent: #7c5cff;
--accent-2: #29d3b2;
--danger: #ff5f7a;
--shadow: 0 20px 60px rgba(0,0,0,0.35);
--radius: 22px;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background:
radial-gradient(circle at top left, rgba(124,92,255,0.30), transparent 34%),
radial-gradient(circle at right, rgba(41,211,178,0.16), transparent 28%),
linear-gradient(180deg, #12162a 0%, #0f1220 100%);
color: var(--text);
display: grid;
place-items: center;
padding: 20px;
}
.app {
width: min(1100px, 100%);
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 20px;
}
.card {
background: var(--panel);
border: 1px solid rgba(255,255,255,0.12);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
overflow: hidden;
}
.hero {
padding: 28px;
min-height: 620px;
display: flex;
flex-direction: column;
gap: 18px;
}
.sidebar {
padding: 22px;
min-height: 620px;
display: flex;
flex-direction: column;
gap: 18px;
}
h1 {
margin: 0;
font-size: clamp(30px, 4vw, 48px);
line-height: 1.05;
letter-spacing: -0.04em;
}
.sub {
margin: 0;
color: var(--muted);
font-size: 15px;
line-height: 1.55;
}
.dropzone {
border: 1.5px dashed rgba(255,255,255,0.22);
border-radius: 18px;
padding: 18px;
min-height: 150px;
display: grid;
place-items: center;
text-align: center;
transition: 0.2s ease;
background: rgba(255,255,255,0.04);
}
.dropzone.dragover {
border-color: var(--accent-2);
background: rgba(41,211,178,0.10);
transform: scale(1.01);
}
.dropzone strong { display: block; font-size: 18px; margin-bottom: 8px; }
.dropzone span { color: var(--muted); font-size: 14px; }
.row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
}
.input, .button, .select {
border: 0;
outline: none;
border-radius: 14px;
font-size: 14px;
}
.input, .select {
width: 100%;
background: rgba(255,255,255,0.08);
color: var(--text);
padding: 14px 16px;
border: 1px solid rgba(255,255,255,0.10);
}
.input::placeholder { color: rgba(244,247,251,0.45); }
.button {
cursor: pointer;
padding: 14px 18px;
background: linear-gradient(135deg, var(--accent), #5a8cff);
color: white;
font-weight: 700;
transition: transform 0.15s ease, filter 0.15s ease;
white-space: nowrap;
}
.button:hover { transform: translateY(-1px); filter: brightness(1.05); }
.button:active { transform: translateY(0px) scale(0.99); }
.button.secondary {
background: rgba(255,255,255,0.10);
color: var(--text);
border: 1px solid rgba(255,255,255,0.14);
}
.button.danger {
background: rgba(255,95,122,0.16);
color: #ffd8df;
border: 1px solid rgba(255,95,122,0.25);
}
.player {
margin-top: auto;
padding: 20px;
border-radius: 20px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.10);
}
.now {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 16px;
}
.cover {
width: 64px;
height: 64px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(124,92,255,0.95), rgba(41,211,178,0.9));
display: grid;
place-items: center;
font-size: 24px;
flex: 0 0 auto;
box-shadow: 0 12px 30px rgba(124,92,255,0.25);
}
.track-meta {
min-width: 0;
flex: 1;
}
.track-title {
margin: 0;
font-size: 17px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.track-subtitle {
margin: 4px 0 0;
color: var(--muted);
font-size: 13px;
}
.progress-wrap {
margin: 12px 0 10px;
}
.bar {
width: 100%;
height: 10px;
appearance: none;
background: rgba(255,255,255,0.10);
border-radius: 999px;
overflow: hidden;
outline: none;
}
.bar::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
border: 3px solid var(--accent);
box-shadow: 0 0 0 6px rgba(124,92,255,0.15);
cursor: pointer;
}
.time-row {
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 12px;
margin-top: 8px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.controls .button {
flex: 1 1 120px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.list-header h2 {
margin: 0;
font-size: 20px;
letter-spacing: -0.02em;
}
.count {
color: var(--muted);
font-size: 13px;
}
.playlist {
display: flex;
flex-direction: column;
gap: 10px;
overflow: auto;
padding-right: 4px;
max-height: 470px;
}
.item {
display: grid;
grid-template-columns: 44px 1fr auto;
gap: 12px;
align-items: center;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 12px;
cursor: pointer;
transition: 0.18s ease;
}
.item:hover { transform: translateY(-1px); background: rgba(255,255,255,0.09); }
.item.active {
border-color: rgba(41,211,178,0.45);
background: rgba(41,211,178,0.12);
}
.mini-cover {
width: 44px;
height: 44px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(124,92,255,0.85), rgba(41,211,178,0.78));
display: grid;
place-items: center;
font-size: 18px;
flex: 0 0 auto;
}
.name {
margin: 0;
font-size: 14px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
.small-btn {
border: 0;
background: rgba(255,255,255,0.10);
color: var(--text);
width: 38px;
height: 38px;
border-radius: 12px;
cursor: pointer;
display: grid;
place-items: center;
}
.empty {
color: var(--muted);
text-align: center;
padding: 28px 16px;
border: 1px dashed rgba(255,255,255,0.15);
border-radius: 18px;
background: rgba(255,255,255,0.04);
}
.hint {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
}
audio { display: none; }
@media (max-width: 900px) {
.app { grid-template-columns: 1fr; }
.hero, .sidebar { min-height: auto; }
.playlist { max-height: 320px; }
}
</style>
</head>
<body>
<div class="app">
<section class="card hero">
<div>
<h1>Музыкальный плеер</h1>
<p class="sub">Загружай файлы с компьютера или вставляй ссылку на аудио. Это не настоящий стриминг-сервис, а удобный HTML5-плеер для проигрывания треков прямо в браузере.</p>
</div>
<div class="dropzone" id="dropzone">
<div>
<strong>Перетащи аудиофайл сюда</strong>
<span>или выбери файл кнопкой ниже</span>
</div>
</div>
<div class="row">
<input id="fileInput" type="file" accept="audio/*" class="input" />
<button class="button secondary" id="clearBtn" title="Очистить список">Очистить</button>
</div>
<div class="row">
<input id="urlInput" class="input" type="url" placeholder="Вставь ссылку на аудио или поток (.mp3, .wav, .ogg, .m3u8 и т.п.)" />
<button class="button" id="addUrlBtn">Добавить</button>
</div>
<div class="player">
<div class="now">
<div class="cover">♫</div>
<div class="track-meta">
<p class="track-title" id="currentTitle">Ничего не выбрано</p>
<p class="track-subtitle" id="currentSource">Добавь трек, чтобы начать</p>
</div>
</div>
<div class="progress-wrap">
<input id="seekBar" class="bar" type="range" min="0" max="100" value="0" disabled />
<div class="time-row">
<span id="currentTime">00:00</span>
<span id="duration">00:00</span>
</div>
</div>
<div class="controls">
<button class="button secondary" id="prevBtn">⏮ Назад</button>
<button class="button" id="playBtn">▶️ Play</button>
<button class="button secondary" id="nextBtn">⏭ Далее</button>
<button class="button danger" id="removeBtn">Удалить текущий</button>
</div>
<p class="hint">Подсказка: кликни по треку в списке, чтобы выбрать его. Клавиши Space — play/pause, ←/→ — перемотка на 5 секунд.</p>
</div>
<audio id="audio"></audio>
</section>
<aside class="card sidebar">
<div class="list-header">
<h2>Плейлист</h2>
<div class="count" id="trackCount">0 треков</div>
</div>
<div id="playlist" class="playlist">
<div class="empty">Список пуст. Добавь первый файл или ссылку справа/сверху.</div>
</div>
</aside>
</div>
<script>
const audio = document.getElementById('audio');
const fileInput = document.getElementById('fileInput');
const urlInput = document.getElementById('urlInput');
const addUrlBtn = document.getElementById('addUrlBtn');
const clearBtn = document.getElementById('clearBtn');
const removeBtn = document.getElementById('removeBtn');
const prevBtn = document.getElementById('prevBtn');
const playBtn = document.getElementById('playBtn');
const nextBtn = document.getElementById('nextBtn');
const seekBar = document.getElementById('seekBar');
const currentTimeEl = document.getElementById('currentTime');
const durationEl = document.getElementById('duration');
const currentTitle = document.getElementById('currentTitle');
const currentSource = document.getElementById('currentSource');
const playlistEl = document.getElementById('playlist');
const trackCount = document.getElementById('trackCount');
const dropzone = document.getElementById('dropzone');
let tracks = [];
let currentIndex = -1;
let objectUrls = new Set();
function formatTime(seconds) {
if (!isFinite(seconds)) return '00:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
function updateTrackCount() {
trackCount.textContent = `${tracks.length} трек${tracks.length === 1 ? '' : tracks.length < 5 ? 'а' : 'ов'}`;
}
function renderPlaylist() {
playlistEl.innerHTML = '';
if (tracks.length === 0) {
playlistEl.innerHTML = '<div class="empty">Список пуст. Добавь первый файл или ссылку сверху.</div>';
updateTrackCount();
return;
}
tracks.forEach((track, index) => {
const item = document.createElement('div');
item.className = 'item' + (index === currentIndex ? ' active' : '');
item.innerHTML = `
<div class="mini-cover">♫</div>
<div>
<p class="name">${escapeHtml(track.name)}</p>
<div class="meta">${escapeHtml(track.type)} • ${escapeHtml(track.sourceLabel)}</div>
</div>
<button class="small-btn" title="Удалить">✕</button>
`;
item.addEventListener('click', (e) => {
if (e.target.closest('button')) {
removeTrack(index);
return;
}
playTrack(index);
});
playlistEl.appendChild(item);
});
updateTrackCount();
}
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function addTrack(track) {
tracks.push(track);
if (currentIndex === -1) {
currentIndex = 0;
loadCurrentTrack(false);
}
renderPlaylist();
}
function loadCurrentTrack(autoplay = true) {
if (currentIndex < 0 || currentIndex >= tracks.length) {
audio.removeAttribute('src');
currentTitle.textContent = 'Ничего не выбрано';
currentSource.textContent = 'Добавь трек, чтобы начать';
playBtn.textContent = '▶️ Play';
seekBar.value = 0;
seekBar.disabled = true;
currentTimeEl.textContent = '00:00';
durationEl.textContent = '00:00';
renderPlaylist();
return;
}
const track = tracks[currentIndex];
audio.src = track.url;
audio.load();
currentTitle.textContent = track.name;
currentSource.textContent = track.sourceLabel;
seekBar.disabled = false;
renderPlaylist();
if (autoplay) {
audio.play().catch(() => {
playBtn.textContent = '▶️ Play';
});
}
playBtn.textContent = '⏸ Pause';
}
function playTrack(index) {
currentIndex = index;
loadCurrentTrack(true);
}
function nextTrack() {
if (!tracks.length) return;
currentIndex = (currentIndex + 1) % tracks.length;
loadCurrentTrack(true);
}
function prevTrack() {
if (!tracks.length) return;
currentIndex = (currentIndex - 1 + tracks.length) % tracks.length;
loadCurrentTrack(true);
}
function removeTrack(index) {
const track = tracks[index];
if (track?.revoke && track.url.startsWith('blob:')) {
URL.revokeObjectURL(track.url);
objectUrls.delete(track.url);
}
tracks.splice(index, 1);
if (tracks.length === 0) {
currentIndex = -1;
audio.pause();
loadCurrentTrack(false);
return;
}
if (index < currentIndex) currentIndex--;
else if (index === currentIndex) {
currentIndex = Math.min(currentIndex, tracks.length - 1);
loadCurrentTrack(true);
}
renderPlaylist();
}
function clearAll() {
objectUrls.forEach((u) => URL.revokeObjectURL(u));
objectUrls.clear();
tracks = [];
currentIndex = -1;
audio.pause();
loadCurrentTrack(false);
}
fileInput.addEventListener('change', (e) => {
const files = [...e.target.files || []];
for (const file of files) {
const url = URL.createObjectURL(file);
objectUrls.add(url);
addTrack({
name: file.name,
url,
type: file.type || 'audio',
sourceLabel: 'Локальный файл',
revoke: true,
});
}
fileInput.value = '';
});
addUrlBtn.addEventListener('click', () => {
const url = urlInput.value.trim();
if (!url) return;
const inferredName = url.split('/').pop()?.split('?')[0] || 'Audio stream';
addTrack({
name: inferredName,
url,
type: 'URL/stream',
sourceLabel: 'Ссылка',
revoke: false,
});
urlInput.value = '';
});
clearBtn.addEventListener('click', clearAll);
removeBtn.addEventListener('click', () => {
if (currentIndex >= 0) removeTrack(currentIndex);
});
prevBtn.addEventListener('click', prevTrack);
nextBtn.addEventListener('click', nextTrack);
playBtn.addEventListener('click', () => {
if (!tracks.length) return;
if (audio.paused) {
audio.play().catch(() => {});
} else {
audio.pause();
}
});
audio.addEventListener('play', () => {
playBtn.textContent = '⏸ Pause';
});
audio.addEventListener('pause', () => {
playBtn.textContent = '▶️ Play';
});
audio.addEventListener('loadedmetadata', () => {
durationEl.textContent = formatTime(audio.duration);
seekBar.max = String(Math.floor(audio.duration || 0));
seekBar.value = '0';
});
audio.addEventListener('timeupdate', () => {
if (!isNaN(audio.currentTime)) {
seekBar.value = String(Math.floor(audio.currentTime));
currentTimeEl.textContent = formatTime(audio.currentTime);
}
});
audio.addEventListener('ended', nextTrack);
seekBar.addEventListener('input', () => {
audio.currentTime = Number(seekBar.value);
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
const files = [...e.dataTransfer.files || []].filter(f => f.type.startsWith('audio/'));
for (const file of files) {
const url = URL.createObjectURL(file);
objectUrls.add(url);
addTrack({
name: file.name,
url,
type: file.type || 'audio',
sourceLabel: 'Drag & Drop',
revoke: true,
});
}
});
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea')) return;
if (e.code === 'Space') {
e.preventDefault();
if (!tracks.length) return;
if (audio.paused) audio.play().catch(() => {}); else audio.pause();
}
if (e.key === 'ArrowRight') {
audio.currentTime = Math.min(audio.duration || Infinity, audio.currentTime + 5);
}
if (e.key === 'ArrowLeft') {
audio.currentTime = Math.max(0, audio.currentTime - 5);
}
});
renderPlaylist();
</script>
</body>
</html>