document.addEventListener("DOMContentLoaded", () => { const downloadForm = document.getElementById("downloadForm"); const urlInput = document.getElementById("urlInput"); const syncLikesBtn = document.getElementById("syncLikesBtn"); const tasksList = document.getElementById("tasksList"); const historyBody = document.getElementById("historyBody"); const tracksCount = document.getElementById("tracksCount"); // Search elements const searchCard = document.getElementById("searchCard"); const searchQueryLabel = document.getElementById("searchQueryLabel"); const searchLoading = document.getElementById("searchLoading"); const searchTracksList = document.getElementById("searchTracksList"); const searchAlbumsList = document.getElementById("searchAlbumsList"); const closeSearchBtn = document.getElementById("closeSearchBtn"); // Load history and active tasks fetchHistory(); fetchTasks(); setInterval(fetchTasks, 2000); setInterval(fetchHistory, 10000); // Form submit downloadForm.addEventListener("submit", async (e) => { e.preventDefault(); const val = urlInput.value.trim(); if (!val) return; // Check if it's a direct URL if (val.startsWith("http://") || val.startsWith("https://")) { await startDownload(val); urlInput.value = ""; } else { // It's a search query! await performSearch(val); } }); // Close search closeSearchBtn.addEventListener("click", () => { searchCard.style.display = "none"; }); // Perform search async function performSearch(query) { searchCard.style.display = "block"; searchQueryLabel.textContent = `«${query}»`; searchLoading.style.display = "block"; searchTracksList.innerHTML = ""; searchAlbumsList.innerHTML = ""; try { const res = await fetch(`api/search?q=${encodeURIComponent(query)}`); const data = await res.json(); searchLoading.style.display = "none"; renderSearchResults(data); } catch (err) { searchLoading.style.display = "none"; searchTracksList.innerHTML = `

Ошибка поиска: ${err}

`; } } function renderSearchResults(data) { const tracks = data.tracks || []; const albums = data.albums || []; // Tracks if (tracks.length === 0) { searchTracksList.innerHTML = `

Треков не найдено

`; } else { searchTracksList.innerHTML = tracks.map(t => { const explicitBadge = t.explicit_status === 'explicit' ? `🔞 18+ Explicit` : (t.explicit_status === 'clean' ? `⚠️ Clean (Цензура)` : ''); const versionBadge = t.version ? `${escapeHtml(t.version)}` : ''; return `
${escapeHtml(t.title)} ${explicitBadge} ${versionBadge}
${t.service_name} • ${escapeHtml(t.artist)} • ${escapeHtml(t.album || 'Single')} (${t.duration})
💾 Скачать
`; }).join(""); } // Albums if (albums.length === 0) { document.getElementById("searchAlbumsSection").style.display = "none"; } else { document.getElementById("searchAlbumsSection").style.display = "block"; searchAlbumsList.innerHTML = albums.map(a => `
${escapeHtml(a.title)} ${a.service_name} • ${a.year || ''} • ${a.track_count ? a.track_count + ' треков' : ''}
`).join(""); } } // Audio Preview Player let currentPlayingUrl = null; window.playPreviewTrack = function(url, title, artist, cover, explicitStatus, previewUrl) { const bar = document.getElementById("audioPlayerBar"); const audio = document.getElementById("globalAudioPlayer"); const playerCover = document.getElementById("playerCover"); const playerTitle = document.getElementById("playerTitle"); const playerArtist = document.getElementById("playerArtist"); const playerBadge = document.getElementById("playerExplicitBadge"); const playerImportBtn = document.getElementById("playerImportBtn"); const playerDownloadBtn = document.getElementById("playerDownloadBtn"); if (currentPlayingUrl === url && !audio.paused) { audio.pause(); return; } currentPlayingUrl = url; playerCover.src = cover || "static/placeholder.png"; playerTitle.textContent = title; playerArtist.textContent = artist; if (explicitStatus === "explicit") { playerBadge.className = "badge badge-explicit"; playerBadge.textContent = "🔞 18+ Explicit (Без цензуры)"; playerBadge.style.display = "inline-flex"; } else if (explicitStatus === "clean") { playerBadge.className = "badge badge-clean"; playerBadge.textContent = "⚠️ Clean (С цензурой)"; playerBadge.style.display = "inline-flex"; } else { playerBadge.style.display = "none"; } playerImportBtn.onclick = () => handleImport(url); playerDownloadBtn.onclick = () => { window.location.href = "api/direct-download?url=" + encodeURIComponent(url); }; bar.style.display = "flex"; // Choose source: Apple 30s direct preview or server stream with HTTP Range const streamSrc = previewUrl || ("api/stream?url=" + encodeURIComponent(url)); audio.src = streamSrc; audio.play().catch(e => console.warn("Playback error:", e)); }; const playerCloseBtn = document.getElementById("playerCloseBtn"); if (playerCloseBtn) { playerCloseBtn.addEventListener("click", () => { const audio = document.getElementById("globalAudioPlayer"); audio.pause(); audio.src = ""; currentPlayingUrl = null; document.getElementById("audioPlayerBar").style.display = "none"; }); } // Global handler for import button window.handleImport = async function(url) { await startDownload(url); }; async function startDownload(url) { try { const res = await fetch("api/download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }) }); const data = await res.json(); if (data.task_id) { fetchTasks(); window.scrollTo({ top: tasksList.offsetTop - 100, behavior: "smooth" }); } else { alert(data.error || "Ошибка запуска загрузки"); } } catch (err) { alert("Ошибка сети: " + err); } } // Sync likes button syncLikesBtn.addEventListener("click", async () => { if (!confirm("Запустить синхронизацию плейлиста «Мне нравится» из Яндекс Музыки?")) return; try { const res = await fetch("api/sync/yandex-likes", { method: "POST" }); const data = await res.json(); if (data.task_id) { fetchTasks(); } } catch (err) { alert("Ошибка запуска синхронизации: " + err); } }); async function fetchTasks() { try { const res = await fetch("api/tasks"); const tasks = await res.json(); renderTasks(tasks); } catch (err) { console.error(err); } } function renderTasks(tasks) { if (!tasks || tasks.length === 0) { tasksList.innerHTML = `

Нет активных загрузок

`; return; } tasksList.innerHTML = tasks.map(t => { const percent = t.total > 0 ? Math.round((t.progress / t.total) * 100) : 0; return `
${escapeHtml(t.target)} ${t.status}
${t.progress}/${t.total} (${percent}%) • ${escapeHtml(t.current_item || '')}
`; }).join(""); } async function fetchHistory() { try { const res = await fetch("api/history"); const data = await res.json(); if (tracksCount) tracksCount.textContent = `Загружено: ${data.total || 0}`; renderHistory(data.tracks || []); } catch (err) { console.error(err); } } function renderHistory(tracks) { if (!tracks || tracks.length === 0) { historyBody.innerHTML = `Пока нет загрузок`; return; } historyBody.innerHTML = tracks.map(t => ` ${t.service} ${escapeHtml(t.artist)} ${escapeHtml(t.title)} ${escapeHtml(t.album || 'Singles')} ${formatDate(t.downloaded_at)} `).join(""); } function escapeHtml(str) { if (!str) return ""; return str.replace(/&/g, "&").replace(//g, ">"); } function escapeAttr(str) { if (!str) return ""; return str.replace(/'/g, "\\'").replace(/"/g, """); } function formatDate(isoStr) { if (!isoStr) return ""; 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"); const waveMoodSelect = document.getElementById("waveMoodSelect"); const waveContextLabel = document.getElementById("waveContextLabel"); fetchWaveStatus(); if (waveGenerateBtn) { waveGenerateBtn.addEventListener("click", async () => { waveGenerateBtn.disabled = true; const mood = waveMoodSelect ? waveMoodSelect.value : "auto"; if (waveLoadingText) { waveLoadingText.innerHTML = ` Qwen 2.5 отбирает треки под вайб...`; waveLoadingText.style.display = "inline-block"; } try { const res = await fetch("api/wave/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mood: mood }) }); if (res.status === 401) { window.location.href = "login"; return; } const data = await res.json(); alert(`🌊 Волна обновлена под настроение [${data.mood_label || mood}]!\nДобавлено новых треков: ${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" }); if (res.status === 401) { window.location.href = "login"; return; } 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"); if (res.status === 401) { window.location.href = "login"; return; } 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} МБ`; // Context label if (waveContextLabel && data.current_context) { waveContextLabel.textContent = data.current_context.label || "🕒 Авто"; waveContextLabel.title = data.current_context.desc || ""; } // Anchors if (waveAnchorsContainer && waveAnchorsList) { const anchors = data.anchor_artists || []; if (anchors.length > 0) { waveAnchorsContainer.style.display = "block"; waveAnchorsList.innerHTML = anchors.map(a => `${escapeHtml(a)}`).join(""); } else { waveAnchorsContainer.style.display = "none"; } } // Tracks list if (waveTracksList) { const tracks = data.tracks || []; if (tracks.length === 0) { waveTracksList.innerHTML = `

Волна пуста. Нажмите «Обновить Волну» для генерации первого потока.

`; } else { waveTracksList.innerHTML = tracks.map(t => `
${escapeHtml(t.title)} ${escapeHtml(t.artist)} • ${t.size_mb} МБ (${t.age_days} дн. назад)
`).join(""); } } } // Window actions for Like, Dislike & Delete window.likeWaveTrack = async function(filename) { if (!filename) return; try { const res = await fetch("api/wave/like", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: filename }) }); if (res.status === 401) { window.location.href = "login"; return; } const data = await res.json(); alert(`⭐ Трек [${data.artist} — ${data.title}] перенесён в вашу постоянную библиотеку Navidrome!\nСлот в Волне автоматически обновлен.`); await fetchWaveStatus(); await fetchHistory(); } catch (e) { alert("Ошибка добавления в коллекцию: " + e); } }; window.dislikeWaveTrack = async function(filename) { if (!filename) return; if (!confirm("Дизлайкнуть трек? Он будет удалён из Волны и занесён в чёрный список рекомендаций.")) return; try { const res = await fetch("api/wave/dislike", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: filename }) }); if (res.status === 401) { window.location.href = "login"; return; } const data = await res.json(); await fetchWaveStatus(); } catch (e) { alert("Ошибка дизлайка: " + e); } }; window.deleteHistoryTrack = async function(filePath) { if (!filePath) return; if (!confirm("Удалить этот трек из библиотеки и стереть файл с диска?")) return; try { const res = await fetch(`api/track?file_path=${encodeURIComponent(filePath)}`, { method: "DELETE" }); if (res.status === 401) { window.location.href = "login"; return; } if (res.ok) { await fetchHistory(); await fetchWaveStatus(); } else { const err = await res.json(); alert("Ошибка удаления: " + (err.detail || res.statusText)); } } catch (e) { alert("Ошибка сети при удалении: " + e); } }; // --- AI Radio Host Controller (Phase 3) --- const radioGenerateBtn = document.getElementById("radioGenerateBtn"); const radioVoiceSelect = document.getElementById("radioVoiceSelect"); const radioStatusLabel = document.getElementById("radioStatusLabel"); const radioClipsCountLabel = document.getElementById("radioClipsCountLabel"); const radioSegmentsContainer = document.getElementById("radioSegmentsContainer"); const radioSegmentsList = document.getElementById("radioSegmentsList"); fetchRadioStatus(); if (radioGenerateBtn) { radioGenerateBtn.addEventListener("click", async () => { radioGenerateBtn.disabled = true; const voice = radioVoiceSelect ? radioVoiceSelect.value : "ru-RU-DmitryNeural"; if (radioStatusLabel) { radioStatusLabel.textContent = "🎙️ Ведущий пишет реплики и сводит эфир..."; radioStatusLabel.className = "badge badge-llm"; } try { const res = await fetch("api/radio/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ voice: voice }) }); if (res.status === 401) { window.location.href = "login"; return; } const data = await res.json(); if (data.status === "ok") { alert(`📻 Радиоэфир успешно сформирован!\nСгенерировано реплик ведущего: ${data.dj_segments_count}\nПлейлист «AI Радио.m3u8» готов в Navidrome.`); if (data.segments && data.segments.length > 0 && radioSegmentsContainer && radioSegmentsList) { radioSegmentsContainer.style.display = "block"; radioSegmentsList.innerHTML = data.segments.map(s => `
«${escapeHtml(s.text)}»
🎵 Перед треком: ${escapeHtml(s.before_track)}
`).join(""); } } else { alert("Внимание: " + (data.message || "Ошибка генерации")); } await fetchRadioStatus(); } catch (err) { alert("Ошибка генерации радиоэфира: " + err); } finally { radioGenerateBtn.disabled = false; } }); } async function fetchRadioStatus() { try { const res = await fetch("api/radio/status"); if (res.status === 401) return; const data = await res.json(); if (radioClipsCountLabel) { radioClipsCountLabel.textContent = `${data.speech_clips_count || 0} записей`; } if (radioStatusLabel) { if (data.playlist_ready && data.speech_clips_count > 0) { radioStatusLabel.textContent = "🟢 Эфир в ротации"; radioStatusLabel.className = "badge badge-success"; } else { radioStatusLabel.textContent = "🟡 Ожидает генерации"; radioStatusLabel.className = "badge badge-warning"; } } } catch (e) { console.error("Radio status error:", e); } } });