feat(wave): implement Phase 1 Smart Wave recommendations and buffer rotation
This commit is contained in:
1 parent
aa6c605d14
commit
9fbe372fab
10 files changed
+879
No files matched your search
@@ -297,4 +297,102 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const d = new Date(isoStr);
|
||||
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// --- Smart Wave Logic ---
|
||||
const waveCountLabel = document.getElementById("waveCountLabel");
|
||||
const waveSizeLabel = document.getElementById("waveSizeLabel");
|
||||
const waveAnchorsContainer = document.getElementById("waveAnchorsContainer");
|
||||
const waveAnchorsList = document.getElementById("waveAnchorsList");
|
||||
const waveTracksList = document.getElementById("waveTracksList");
|
||||
const waveLoadingText = document.getElementById("waveLoadingText");
|
||||
const waveGenerateBtn = document.getElementById("waveGenerateBtn");
|
||||
const waveRotateBtn = document.getElementById("waveRotateBtn");
|
||||
|
||||
fetchWaveStatus();
|
||||
|
||||
if (waveGenerateBtn) {
|
||||
waveGenerateBtn.addEventListener("click", async () => {
|
||||
waveGenerateBtn.disabled = true;
|
||||
if (waveLoadingText) waveLoadingText.style.display = "inline-block";
|
||||
try {
|
||||
const res = await fetch("api/wave/generate", { method: "POST" });
|
||||
const data = await res.json();
|
||||
alert(`Волна обновлена! Добавлено: ${data.added_count || 0} треков.`);
|
||||
await fetchWaveStatus();
|
||||
} catch (err) {
|
||||
alert("Ошибка генерации волны: " + err);
|
||||
} finally {
|
||||
waveGenerateBtn.disabled = false;
|
||||
if (waveLoadingText) waveLoadingText.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (waveRotateBtn) {
|
||||
waveRotateBtn.addEventListener("click", async () => {
|
||||
waveRotateBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch("api/wave/rotate", { method: "POST" });
|
||||
const data = await res.json();
|
||||
const promMsg = (data.promoted_count > 0) ? `⭐ Перенесено в библиотеку: ${data.promoted_count}` : "Нет лайкнутых для переноса";
|
||||
const purMsg = (data.purged_count > 0) ? `🗑️ Удалено устаревших: ${data.purged_count}` : "Нет устаревших";
|
||||
alert(`Ротация завершена!\n${promMsg}\n${purMsg}`);
|
||||
await fetchWaveStatus();
|
||||
} catch (err) {
|
||||
alert("Ошибка ротации: " + err);
|
||||
} finally {
|
||||
waveRotateBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchWaveStatus() {
|
||||
try {
|
||||
const res = await fetch("api/wave/status");
|
||||
const data = await res.json();
|
||||
renderWaveStatus(data);
|
||||
} catch (err) {
|
||||
console.error("Wave status error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWaveStatus(data) {
|
||||
if (waveCountLabel) waveCountLabel.textContent = `${data.count || 0} / ${data.capacity || 25} треков`;
|
||||
if (waveSizeLabel) waveSizeLabel.textContent = `${data.total_size_mb || 0} МБ`;
|
||||
|
||||
// Anchors
|
||||
if (waveAnchorsContainer && waveAnchorsList) {
|
||||
const anchors = data.anchor_artists || [];
|
||||
if (anchors.length > 0) {
|
||||
waveAnchorsContainer.style.display = "block";
|
||||
waveAnchorsList.innerHTML = anchors.map(a => `<span class="tag-anchor">${escapeHtml(a)}</span>`).join("");
|
||||
} else {
|
||||
waveAnchorsContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks list
|
||||
if (waveTracksList) {
|
||||
const tracks = data.tracks || [];
|
||||
if (tracks.length === 0) {
|
||||
waveTracksList.innerHTML = `<p class="empty-state">Волна пуста. Нажмите «Обновить Волну» для генерации первого потока.</p>`;
|
||||
} else {
|
||||
waveTracksList.innerHTML = tracks.map(t => `
|
||||
<div class="wave-track-item">
|
||||
<div style="min-width: 0; display: flex; flex-direction: column; gap: 0.15rem;">
|
||||
<strong style="font-size: 0.88rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(t.title)}</strong>
|
||||
<span style="font-size: 0.78rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
${escapeHtml(t.artist)} • ${t.size_mb} МБ (${t.age_days} дн. назад)
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.4rem; flex-shrink: 0;">
|
||||
<button class="btn-play-preview" onclick="playPreviewTrack('${escapeAttr(t.path)}', '${escapeAttr(t.title)}', '${escapeAttr(t.artist)}', '', '', 'api/stream?url=' + encodeURIComponent('${escapeAttr(t.path)}'))">
|
||||
▶ Слушать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -610,3 +610,124 @@ input[type="text"]:focus {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smart Wave Styles */
|
||||
.badge-wave {
|
||||
background-color: rgba(6, 182, 212, 0.2);
|
||||
color: #22d3ee;
|
||||
border: 1px solid rgba(6, 182, 212, 0.4);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wave-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.wave-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.wave-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wave-stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
background-color: #0f172a;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.wave-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.wave-stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.wave-stat strong {
|
||||
font-size: 0.95rem;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.wave-stat strong a {
|
||||
color: #22d3ee;
|
||||
text-decoration: none;
|
||||
}
|
||||
.wave-stat strong a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wave-anchors-tags {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.tag-anchor {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
color: #94a3b8;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.wave-tracks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.wave-tracks-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wave-track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #0f172a;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.8rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--accent-color);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
Reference in new issue
Block a user