feat(search): add multi-source search across Yandex, Apple, YouTube with direct download & import
This commit is contained in:
1 parent
c098376aa4
commit
49bd123a03
5 files changed
+496
-18
No files matched your search
+103
-4
@@ -6,6 +6,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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();
|
||||
@@ -15,9 +23,95 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// Form submit
|
||||
downloadForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) return;
|
||||
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 = `<p class="empty-state">Ошибка поиска: ${err}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResults(data) {
|
||||
const tracks = data.tracks || [];
|
||||
const albums = data.albums || [];
|
||||
|
||||
// Tracks
|
||||
if (tracks.length === 0) {
|
||||
searchTracksList.innerHTML = `<p class="empty-state">Треков не найдено</p>`;
|
||||
} else {
|
||||
searchTracksList.innerHTML = tracks.map(t => `
|
||||
<div class="search-track-row">
|
||||
<div class="track-left">
|
||||
<img class="track-thumb" src="${t.cover || 'static/placeholder.png'}" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'44\' height=\'44\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%2364748b\' stroke-width=\'2\'><rect x=\'3\' y=\'3\' width=\'18\' height=\'18\' rx=\'2\'/><circle cx=\'12\' cy=\'12\' r=\'4\'/></svg>'">
|
||||
<div class="track-details">
|
||||
<span class="track-title">${escapeHtml(t.title)}</span>
|
||||
<span class="track-meta">
|
||||
<span class="tag-badge tag-${t.service}">${t.service_name}</span> • ${escapeHtml(t.artist)} • ${escapeHtml(t.album || 'Single')} (${t.duration})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-actions">
|
||||
<button class="btn-sm btn-import" onclick="handleImport('${escapeAttr(t.url)}')">📥 В Navidrome</button>
|
||||
<a class="btn-sm btn-direct-dl" href="api/direct-download?url=${encodeURIComponent(t.url)}" download>💾 Скачать</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// Albums
|
||||
if (albums.length === 0) {
|
||||
document.getElementById("searchAlbumsSection").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("searchAlbumsSection").style.display = "block";
|
||||
searchAlbumsList.innerHTML = albums.map(a => `
|
||||
<div class="album-card">
|
||||
<img class="album-thumb" src="${a.cover || ''}" onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'120\' height=\'120\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%2364748b\' stroke-width=\'2\'><circle cx=\'12\' cy=\'12\' r=\'10\'/><circle cx=\'12\' cy=\'12\' r=\'3\'/></svg>'">
|
||||
<span class="album-title">${escapeHtml(a.title)}</span>
|
||||
<span class="album-meta">
|
||||
<span class="tag-badge tag-${a.service}">${a.service_name}</span> • ${a.year || ''} • ${a.track_count ? a.track_count + ' треков' : ''}
|
||||
</span>
|
||||
<button class="btn-sm btn-import" style="margin-top: auto;" onclick="handleImport('${escapeAttr(a.url)}')">📥 Импортировать альбом</button>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
@@ -26,15 +120,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.task_id) {
|
||||
urlInput.value = "";
|
||||
fetchTasks();
|
||||
window.scrollTo({ top: tasksList.offsetTop - 100, behavior: "smooth" });
|
||||
} else {
|
||||
alert(data.error || "Ошибка запуска загрузки");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Ошибка сети: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sync likes button
|
||||
syncLikesBtn.addEventListener("click", async () => {
|
||||
@@ -118,6 +212,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return str.replace(/&/g, "&").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);
|
||||
|
||||
Reference in new issue
Block a user