added player

This commit is contained in:
Ultradesu
2026-07-16 18:45:39 +03:00
parent d77a985708
commit 50320c05c6
9 changed files with 645 additions and 13 deletions
+66 -7
View File
@@ -61,10 +61,18 @@
td.actions { text-align: right; white-space: nowrap; }
td .muted { color: var(--muted); }
#toast {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
position: fixed; bottom: 76px; left: 50%; transform: translateX(-50%);
background: var(--danger); color: #fff; padding: 10px 18px; border-radius: 8px;
display: none; max-width: 80vw;
display: none; max-width: 80vw; z-index: 10;
}
#player {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 9;
display: flex; align-items: center; gap: 14px;
padding: 8px 20px; background: var(--panel); border-top: 1px solid var(--border);
}
#player-title { min-width: 200px; max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#player audio { flex: 1; max-width: 640px; height: 36px; }
main { padding-bottom: 90px; }
#toast.ok { background: #3f9d5f; }
dialog {
background: var(--panel); color: var(--text); border: 1px solid var(--border);
@@ -76,7 +84,7 @@
</style>
</head>
<body>
<header><h1>furumi-fd</h1><span>менеджер музыкальной библиотеки (только индекс — файлы не трогает)</span></header>
<header><h1>furumi-fd</h1><span>менеджер музыкальной библиотеки (индекс + прослушивание; файлы никогда не изменяются)</span></header>
<nav id="tabs">
<button data-tab="artists" class="active">Артисты</button>
<button data-tab="releases">Релизы</button>
@@ -190,12 +198,17 @@
<span id="fed-search-meta" style="color:var(--muted);align-self:center"></span>
</div>
<table><thead><tr>
<th>Тип</th><th>Название</th><th>Артисты</th><th class="num">Год</th><th>Детали</th><th>Владелец</th>
<th>Тип</th><th>Название</th><th>Артисты</th><th class="num">Год</th><th>Детали</th><th>Владелец</th><th></th>
</tr></thead><tbody id="fed-results-body"></tbody></table>
<div class="empty" id="fed-results-empty" hidden>Ничего не найдено</div>
</section>
</main>
<div id="player" hidden>
<span id="player-title" class="muted"></span>
<audio id="player-audio" controls></audio>
</div>
<div id="toast"></div>
<dialog id="detail-dialog">
<div style="display:flex;justify-content:space-between;align-items:center">
@@ -250,13 +263,55 @@ async function showDetail(title, path) {
} catch (err) { toast(err.message); }
}
function rowButtons(kind, id) {
return `<td class="actions">
function rowButtons(kind, id, extra = '') {
return `<td class="actions">${extra}
<button class="btn secondary small" onclick="viewItem('${kind}', ${id})">инфо</button>
<button class="btn danger" onclick="deleteItem('${kind}', ${id})">удалить</button>
</td>`;
}
function escAttr(value) {
return esc(value).replaceAll('"', '&quot;');
}
// ---------- player ----------
function playUrl(url, title) {
document.getElementById('player').hidden = false;
document.getElementById('player-title').textContent = title;
const audio = document.getElementById('player-audio');
audio.src = url;
audio.play().catch(err => toast('Не удалось запустить воспроизведение: ' + err.message));
}
document.getElementById('player-audio').addEventListener('error', async () => {
const audio = document.getElementById('player-audio');
if (!audio.src) return;
// The stream endpoint reports problems as JSON; surface them.
try {
const res = await fetch(audio.src, { headers: { range: 'bytes=0-' } });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
toast(data.error || `Ошибка воспроизведения (${res.status})`);
return;
}
} catch { /* fall through to the generic message */ }
toast('Ошибка воспроизведения: формат не поддерживается браузером или поток оборвался');
});
document.addEventListener('click', event => {
const local = event.target.closest('button.play-track');
if (local) {
playUrl(`${API}/tracks/${local.dataset.id}/audio`, local.dataset.title);
return;
}
const fed = event.target.closest('button.play-fed');
if (fed) {
const params = new URLSearchParams({ owner: fed.dataset.owner, item_id: fed.dataset.item });
const from = fed.dataset.own === '1' ? 'своя библиотека' : `пир ${fed.dataset.owner.slice(0, 12)}`;
playUrl(`${API}/federation/stream?${params}`, `${fed.dataset.title}${from}`);
}
});
const KIND_PATHS = { artist: 'artists', release: 'releases', track: 'tracks', file: 'media-files' };
const KIND_NAMES = { artist: 'Артист', release: 'Релиз', track: 'Трек', file: 'Файл' };
const KIND_CONFIRM = {
@@ -355,7 +410,8 @@ async function loadTracks() {
<td class="muted">${esc(releaseTitles[t.release_id] ?? '#' + t.release_id)}</td>
<td class="num">${t.track_number ?? ''}</td>
<td class="num">${fmtDuration(t.duration_seconds)}</td>
${rowButtons('track', t.id)}
${rowButtons('track', t.id,
`<button class="btn secondary small play-track" data-id="${t.id}" data-title="${escAttr(t.title)}">▶</button>`)}
</tr>`);
}
@@ -511,6 +567,9 @@ async function fedSearch() {
<td class="num">${r.year ?? ''}</td>
<td class="muted">${esc(fedDetails(r))}</td>
<td class="muted" title="${esc(r.owner)}">${r.own ? 'вы' : esc(shortId(r.owner))}</td>
<td class="actions">${r.kind === 'track'
? `<button class="btn secondary small play-fed" data-owner="${escAttr(r.owner)}" data-item="${escAttr(r.id)}" data-own="${r.own ? 1 : 0}" data-title="${escAttr(r.name)}">▶</button>`
: ''}</td>
</tr>`);
meta.textContent = `узлов опрошено: ${data.queried_nodes}, ${data.duration_ms} мс`;
} catch (err) {